diff --git a/.gitignore b/.gitignore index 823a81765..eee2f0509 100644 --- a/.gitignore +++ b/.gitignore @@ -32,13 +32,16 @@ __pycache__/ .installed.cfg *.egg +# Docs build venv (see docs/requirements.txt, docs/Makefile) +/.venv/ + # Distribution / packaging build/ dist/ # Sphinx documentation -docs/_build/ -docs/_autosummary/ +docs/source/_build/ +docs/source/_autosummary/ ### macOS template # General diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ede209771..955709c85 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,10 +9,12 @@ build: os: ubuntu-22.04 tools: python: "3.7" + apt_packages: + - pandoc # required by nbsphinx to render the tutorial notebooks -# Build documentation in the docs/ directory with Sphinx +# Build documentation in the docs/source/ directory with Sphinx sphinx: - configuration: docs/conf.py + configuration: docs/source/conf.py # Optionally build your docs in additional formats such as PDF and ePub formats: [] diff --git a/docs/Makefile b/docs/Makefile index e9e02455b..57e92e5e0 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -3,7 +3,10 @@ # You can set these variables from the command line. SPHINXOPTS = -SPHINXBUILD = sphinx-build +# Defaults to the project venv (../.venv, see docs/requirements.txt) if it +# exists, so 'make html' works without manually activating it first. +# Override with 'make SPHINXBUILD=... html' to use a different interpreter/env. +SPHINXBUILD = $(if $(wildcard ../.venv/bin/sphinx-build),../.venv/bin/sphinx-build,sphinx-build) PAPER = BUILDDIR = _build @@ -15,9 +18,9 @@ endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext @@ -85,11 +88,11 @@ htmlhelp: qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + @echo "Build finished; now you can run \"qcollectiongenerator\" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/SphinxApidocTurorial.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/project_name.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/SphinxApidocTurorial.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/project_name.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @@ -104,8 +107,8 @@ devhelp: @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/SphinxApidocTurorial" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/SphinxApidocTurorial" + @echo "# mkdir -p $$HOME/.local/share/devhelp/project_name" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/project_name" @echo "# devhelp" epub: @@ -130,7 +133,7 @@ latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + @echo "platex/dvipdfmx finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index ce009a86a..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,352 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# Sphinx Apidoc Turorial documentation build configuration file, created by -# sphinx-quickstart on Fri Jan 8 20:52:00 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -import os -import pathlib - -## Make your modules available in sys.path -here = pathlib.Path(__file__).parent.resolve() -sys.path.insert(0, (here / '../qick_lib').resolve().as_posix()) -print(sys.path) - -def get_version(rel_path): - """ - qick_lib/qick/VERSION is a text file containing only the version number. - """ - return (here / rel_path).read_text().strip() - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - ## Include autosymmary - 'sphinx.ext.autosummary', - 'sphinx.ext.napoleon', - 'sphinx.ext.extlinks', -] - -## Include Python objects as they appear in source files -## Default: alphabetically ('alphabetical') -autodoc_member_order = 'bysource' -## Default flags used by autodoc directives -autodoc_default_options = { - 'members': True, - 'show-inheritance': True, -} - -autodoc_mock_imports = ["pynq", "xrfclk", "xrfdc", "cffi", "Pyro4", "psutil"] - -## Generate autodoc stubs with summaries from code -autosummary_generate = True - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'qick' -copyright = 'openquantumhardware' -author = 'openquantumhardware' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = get_version("../qick_lib/qick/VERSION") -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'en' - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'sphinx_rtd_theme' - -html_theme_options = { - 'logo_only': True, -} -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -html_logo = "../graphics/logoQICK.svg" - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - -# html_css_files = ['custom.css'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -## Sidebars configuration for alabaster theme - -html_sidebars = { - '**': [ - 'about.html', - 'navigation.html', - 'searchbox.html', - ] -} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. - -## I don't like links to page reST sources -html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'SphinxApidocTurorialdoc' - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'SphinxApidocTurorial.tex', 'Sphinx Apidoc Turorial Documentation', - 'Roman Miroshnychenko', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'sphinxapidocturorial', 'Sphinx Apidoc Turorial Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'SphinxApidocTurorial', 'Sphinx Apidoc Turorial Documentation', - author, 'SphinxApidocTurorial', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False - -# Example configuration for intersphinx: refer to the Python standard library. -## Add Python version number to the default address to corretcly reference -## the Python standard library -intersphinx_mapping = {'python': ('https://docs.python.org/3.7', None), - 'pynq': ('https://pynq.readthedocs.io/en/v2.7.0', None), - 'numpy': ('https://numpy.org/doc/1.26', None), - } - -extlinks = {'repofile': ('https://github.com/openquantumhardware/qick/blob/main/%s', - '%s')} - -extlinks_detect_hardcoded_links = True - -linkcheck_ignore = [r'^https://blogs.keysight.com/', # gives 403 error - ] - -def setup(app): - app.add_css_file("custom.css" ) diff --git a/docs/firmware.rst b/docs/firmware.rst deleted file mode 100644 index 401724bd1..000000000 --- a/docs/firmware.rst +++ /dev/null @@ -1,75 +0,0 @@ -Firmware -======== - -This system includes the following components: - -* 1 output channels connected to PMOD0-3 and triggers for Readout Block. -* 7 output channels connected to DACs. -* 2 input channels connected to ADCs. -* 1 instance of tProcessor 64-bit instructions, 32-bit registers. - -Sampling frequency of ADC blocks is given by the variable ``soc.fs_adc``. Sampling frequency of DACs is stored in variable ``soc.fs_adc``. Fast-speed buffers were removed to save memory space. Raw data can be captured after x8 down-sampling. - -Output channels driving DACs use the updated Signal Generator V4, which has the possibility to upload I/Q envelopes, and uses 32-bit resolution for both frequency and phase. The format of the control word was updated accordingly to accomodate the bits. See example asm files for a detailed description of the fields. The maximum length of the I/Q envelopes is given by the variable ``soc.gens[i].MAX_LENGTH``. - -The readout block is actually built around two IPs: readout and average + buffer. Readout block includes a digital down-convertion, FIR filtering and decimation by 8. DDS frequency is configured using a register of the readout block and it is not intended to support real-time frequency hopping as in the Signal Generator side. After frequency shifting, filtering and decimation, the data stream is sent to the Average + Buffer block, which internally can store raw samples or perform the sum of the specified number of samples. The process is started with the external trigger signal, connected to output Channel 0 of tProcessor. The user can opt to route the input, the DDS wave or the frequency shifted signal to the FIR and decimation by 8 stage. This is done using a output selection register of the readout block. Regarding the buffering capabilities, the average section of the block has a buffer of ``soc.avg_bufs[i].AVG_MAX_LENGTH``. - -.. figure:: ../graphics/qsystem-readout.svg - :width: 100% - :align: center - -The tProcessor --------------- - -You also may want to learn more about how the QICK tProcessor works. -In this case, you can reference the `QICK assembly language documentation `_. -Note that this documentation is not up to date with the current version of the QICK tProcessor. -It is made available here as a learning tool for those interested in learning the principles of the tProcessor. -Those who have more specific questions can contact us. - -tProcessor channel assignment ------------------------------ - -tProcessor will be used to control the real-time operation of the experiment. Output channels (AXIS MASTER) of the tProcessor are assigned as follows: - -- Channel 0 : connected to PMOD0 0-3, and triggers for readout. Bits 0-3 are connected to PMOD0, bit 14 is connected to the trigger of the average/buffer block coming from the readout of ADC 224 CH0. Bit 15 is connected to the trigger of the average/buffer block coming from the readout of ADC 224 CH1. -- Channel 1 : connected to Signal Generator V4, which drives DAC 228 CH0. -- Channel 2 : connected to Signal Generator V4, which drives DAC 228 CH1. -- Channel 3 : connected to Signal Generator V4, which drives DAC 228 CH2. -- Channel 4 : connected to Signal Generator V4, which drives DAC 229 CH0. -- Channel 5 : connected to Signal Generator V4, which drives DAC 229 CH1. -- Channel 6 : connected to Signal Generator V4, which drives DAC 229 CH2. -- Channel 7 : connected to Signal Generator V4, which drives DAC 229 CH3. - -**Note** that if you are using the Xilinx XM500 daughter board that comes with the ZCU111, be aware of the filters that are put on that XM500 board: DAC 229 channels 0 and 1 are high pass filtered by a 1 GHz high pass filter, so ensure that signals coming out of channels 4 and 5 are at least 1 GHz. Also, DAC 229 channels 2 and 3 are low pass filtered by a 1 GHz low pass filter, so ensure that signals coming out of channels 6 and 7 are less than 1 GHz. DAC 228 channels 0, 1 and 2 are not filtered by the XM500 daughter board. - -The updated version of the tProcessor has 4 input (AXIS SLAVE) channels, which can be used for feedback. These are 64-bit, and the updated ``read`` instruction can specify channel number and upper/lower 32-bits to be read and written into an internal register. See example below on how to use this new capability. - -* Channel 0 : connected to readout 0, which is driven by ADC 224 CH0 -* Channel 1 : connected to readout 1, which is driven by ADC 224 CH1 - -**Note** that if you are using the Xilinx XM500 daughter board that comes with the ZCU111, be aware of the filters that are put on that XM500 board: ADC 224 channels 0 and 1 are low pass filtered by a 1 GHz low pass filter, so ensure that the signal coming into your XM500 board is less than 1 GHz so that it can be read in properly. - -Signal Generators are organized on the array ``soc.gens``, which is composed of 7 instances. Array index 0 is connected to tProcessor Channel 1, array index 1 is connected to tProcessor Channel 2, and so on. As way of example, let's assume the user needs to create a pulse on DAC 229 CH1 and DAC 229 CH3. These are connected to Channels 5, and 7 or the tProcessor, respectively. However, let's also assume that a gaussian envelope needs to be uploaded into the corresponding signal generator. ``soc.gens[3]`` drives DAC 229 CH1, and ``soc.gens[6]`` drives DAC 229 CH3. - -Similarly, average and buffer inputs blocks are organized on ``soc.avg_bufs`` array, which has two instances of the Average + Buffer block. The user can access them using index 0 and 1. - -Timing ------- - -The clock frequency of the FPGA is 384 MHz. Therefore, each clock cycle has a period of 2.6 ns. - -The DAC speed is ``384*16=6144 MHz`` (resolution ``~163 ps``) and the ADC speed is ``384*8 MHz`` but then the signal is decimated by a factor of ``8`` (resolution ``~2.6 ns``). The minimum DAC pulse length is 16 samples but if you want shorter pulses than that you can pad that pulse with zeros. - - -Firmware parameters -------------------- - -* Pulse memory length: 65536 per channel x2 (I,Q), i.e., 128k total -* Decimated ADC buffer length: 1024 samples per component (I,Q), 2k total -* Accumulated ADC buffer length: 16384 samples per component (I,Q), 32 k total -* tProc program memory length: 8k instructions of 64 bits, 64k Bytes total -* tProc data memory length: 4096 samples of 32 bits, 16k Bytes total -* tProc stack size: 256 samples of 32 bits, 1k Byte total -* Phase conversion from deg to reg: Phase resolution is 32-bit, that is :math:`\Delta \phi = 2 \pi /2^{32}` or :math:`360/2^{32}` -* Gain is 16-bit signed [-32768,32767] diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 673bdcbd1..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,94 +0,0 @@ -.. Sphinx Apidoc Turorial documentation master file, created by - sphinx-quickstart on Fri Jan 8 20:52:00 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -.. toctree:: - :maxdepth: 2 - :hidden: - - quick_start - modules - topics/index - firmware - - contact - -Welcome to the QICK documentation! -================================================= - -.. figure:: ../graphics/QICK.jpg - :width: 100% - :align: center - -.. figure:: ../graphics/ZCU216Board.jpg - :width: 100% - :align: center - -The Quantum Instrumentation Control Kit (QICK for short) is a Xilinx RFSoC-based qubit controller which supports the direct synthesis of control and readout pulses. -The QICK consists of a digital board hosting an RFSoC (RF System-on-Chip) FPGA, custom firmware and software, and an optional companion custom-designed analog front-end board. -All of the schematics, firmware, and software are open-source and available on `Github `_. -We fully support the ZCU111, ZCU216, and RFSoC4x2 evaluation boards, and generally recommend using the newer generation of RFSoCs (ZCU216 and RFSoC4x2) for better overall performance. - -Getting started with QICK -------------------------- - -* First, for a global overview of the QICK and its capabilities, read `our instrumentation paper introducing the QICK `_. - -* If you have an RFSoC board and you want to configure it as a QICK board, follow :doc:`our quick start guide `. - -* After you configure your board, you can test it with `our demo notebooks `_. - These are intended as a tutorial. - The first demos explain important features of the QICK system and walk you through how to write working QICK programs. - The later demos provide examples of useful measurements you might make with the QICK. - We recommend that new users read and understand all of the demos. - -More examples and resources ---------------------------- - -Other examples and tutorials (compatibility with the current QICK software is not guaranteed): - -* `IEEE Quantum Week 2025 `_ -* `IEEE Quantum Week 2024 `_ -* `US QIS Summer School 2024 `_ -* `IEEE Quantum Week 2023 `_ - -Talk to us ------------- - -You can get in touch with the QICK core team and the user community through our :doc:`community contact channels `. - -QICK software -------------- - -`Source code `_ and :doc:`API documentation ` - -:doc:`/topics/index` - -QICK firmware -------------- - -Source code and instructions for compiling it yourself: `qick/firmware `_ - -:doc:`/firmware` - -Extensions beyond the core software and firmware ------------------------------------------------- - -If you want your board's state to persist between notebooks or scripts, you should install Pyro4 on your board and run QICK in a Pyro server: see `our Pyro4 demo notebooks `_ - -If you would like to save the instrument configuration for every measurement using `QCoDeS `_, you can also install `this QCoDeS driver `_. - -If you're interested in using QICK to control and read out NV centers or other quantum defects, you might be interested in `QICK-DAWG `_ which extends QICK with pulses and measurement programs specific to that application. - -If you want to use QICK for control and readout of solid-state spin qubits, `SpinQICK `_ extends the QICK API and provides high-level experiment code. - -QICK papers -------------- - -* For a list of academic papers produced using the QICK system, check out :doc:`our papers page ` - -.. toctree:: - :maxdepth: 2 - - papers diff --git a/docs/modules.rst b/docs/modules.rst deleted file mode 100644 index d4752818a..000000000 --- a/docs/modules.rst +++ /dev/null @@ -1,22 +0,0 @@ -Software API -============ - -.. autosummary:: - :toctree: _autosummary - - qick - qick.qick - qick.ip - qick.drivers.tproc - qick.drivers.generator - qick.drivers.readout - qick.helpers - qick.streamer - qick.pyro - qick.qick_asm - qick.asm_v1 - qick.parser - qick.averager_program - qick.asm_v2 - qick.tprocv2_assembler - qick.rfboard diff --git a/docs/requirements.txt b/docs/requirements.txt index f8cfa6adc..176b84bf6 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,8 @@ numpy>=1.14.2 tqdm +sphinx sphinx-rtd-theme +nbsphinx +myst-parser +ipython +ipykernel diff --git a/docs/_static/custom.css b/docs/source/_static/custom.css similarity index 100% rename from docs/_static/custom.css rename to docs/source/_static/custom.css diff --git a/docs/source/alpha.rst b/docs/source/alpha.rst new file mode 100644 index 000000000..6323aa861 --- /dev/null +++ b/docs/source/alpha.rst @@ -0,0 +1,17 @@ +=============== +Alpha features +=============== + +These are features or components that are still under development. +They are generally only being used by QICK developers and possibly one or two early testers. +These features may require hardware or firmware that is not yet publicly available. +Documentation provided here may not reflect the current state of development. + +XCOM board-to-board communication protocol +========================================== + +.. toctree:: + :maxdepth: 1 + + topics/xcom + topics/XCOM-commands diff --git a/docs/source/beta.rst b/docs/source/beta.rst new file mode 100644 index 000000000..fd570a493 --- /dev/null +++ b/docs/source/beta.rst @@ -0,0 +1,28 @@ +=============== +Beta features +=============== + +These are features or components that are being tested by the QICK community. +There may be bugs, documentation may be incomplete, and updates may break compatiblity with experiment code you write now. +Firmware components in beta are not included in the "standard" firmware bitstreams, but firmwares with these components may be available on request. + +tProcessor v2 +============= + +Version 2 of the QICK tProcessor supports more output channels and allows more flexibility in writing QICK programs. +The assembly language, and therefore the Python API, for tProc v2 are incompatible with those for tProc v1. + +.. toctree:: + :maxdepth: 1 + + tprocv2_trm + topics/asmv2_cheatsheet + topics/tprocv2_migration + +Also see the following pages, which have separate sections for tProc v2 material: + +* :doc:`tutorials/README` +* :doc:`modules` +* :doc:`contributions` + + diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 000000000..d71847b99 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,200 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# QICK documentation build configuration file +# +# This file is execfile()d with the current directory set to its +# containing dir. + +import sys +import os +import pathlib +import shutil + +#Check if Pandoc is installed +if shutil.which('pandoc') is None: + import warnings + warnings.warn( + "pandoc not found. Install it with: sudo apt install pandoc (Linux) " + "or brew install pandoc (macOS). Required for notebook conversion." + ) + +# -- Path setup -------------------------------------------------------------- + +# Make your modules available in sys.path +here = pathlib.Path(__file__).parent.resolve() +sys.path.insert(0, (here / '../../qick_lib').resolve().as_posix()) +print(f"Python path: {sys.path[0]}") + +def get_version(rel_path): + """ + qick_lib/qick/VERSION is a text file containing only the version number. + """ + return (here / rel_path).read_text().strip() + +# -- Project information ----------------------------------------------------- + +project = 'QICK' +copyright = 'OpenQuantumHardware' +author = 'OpenQuantumHardware' + +# The short X.Y version. +version = get_version("../../qick_lib/qick/VERSION") +# The full version, including alpha/beta/rc tags. +release = version + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.autosummary', + 'sphinx.ext.napoleon', + 'sphinx.ext.extlinks', + 'nbsphinx', # For Jupyter notebooks + 'myst_parser', # For Markdown files +] + +# nbsphinx configuration +nbsphinx_execute = 'never' # Don't execute notebooks during build +nbsphinx_allow_errors = True # Continue even if notebooks have errors +nbsphinx_timeout = 120 # Timeout for notebook execution (seconds) + +# autodoc configuration +autodoc_member_order = 'bysource' +autodoc_default_options = { + 'members': True, + 'show-inheritance': True, + 'inherited-members': True, + 'special-members': '__init__', + 'undoc-members': True, + 'exclude-members': '__weakref__' +} + +# Mock imports for modules that are not available during documentation build +autodoc_mock_imports = ["pynq", "xrfclk", "xrfdc", "cffi", "Pyro4", "psutil"] + +# Generate autodoc stubs with summaries from code +autosummary_generate = True + +# Add any paths that contain templates here +templates_path = ['_templates'] + +# The suffix(es) of source filenames +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', + #'.ipynb': 'jupyter_notebooks', +} + +# MyST (Markdown) extensions -- dollarmath lets .md pages use $...$/$$...$$ for equations +myst_enable_extensions = ['dollarmath'] + +# The master toctree document +master_doc = 'index' + +# The language for content +language = 'en' + +# List of patterns to ignore when looking for source files +exclude_patterns = [ + '_build', + 'Thumbs.db', + '.DS_Store', + '**.ipynb_checkpoints', # Exclude Jupyter checkpoint files +] + +# The name of the Pygments (syntax highlighting) style +pygments_style = 'sphinx' + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML pages +html_theme = 'sphinx_rtd_theme' + +# Theme options +html_theme_options = { + 'logo_only': True, + 'navigation_depth': 4, +} + +# The name of an image file to place at the top of the sidebar +html_logo = "../../graphics/logoQICK.svg" + +# Add any paths that contain custom static files +html_static_path = ['_static'] + +# Custom CSS files +html_css_files = ['custom.css'] + +# If true, links to the reST sources are added to the pages +html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer +html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer +html_show_copyright = True + +# Output file base name for HTML help builder +htmlhelp_basename = 'QICKdoc' + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + 'papersize': 'letterpaper', + 'pointsize': '10pt', + 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files +latex_documents = [ + (master_doc, 'QICK.tex', 'QICK Documentation', + author, 'manual'), +] + +# -- Options for manual page output ------------------------------------------ + +man_pages = [ + (master_doc, 'qick', 'QICK Documentation', + [author], 1) +] + +# -- Options for Texinfo output ---------------------------------------------- + +texinfo_documents = [ + (master_doc, 'QICK', 'QICK Documentation', + author, 'QICK', 'Quantum Instrumentation Control Kit', + 'Miscellaneous'), +] + +# -- Intersphinx configuration ----------------------------------------------- + +intersphinx_mapping = { + 'python': ('https://docs.python.org/3.10', None), + 'numpy': ('https://numpy.org/doc/stable', None), + 'pynq': ('https://pynq.readthedocs.io/en/latest', None), +} + +# -- Extlinks configuration -------------------------------------------------- + +extlinks = { + 'repofile': ('https://github.com/openquantumhardware/qick/blob/main/%s', '%s'), +} + +extlinks_detect_hardcoded_links = True + +# -- Linkcheck configuration ------------------------------------------------ + +linkcheck_ignore = [ + r'^https://blogs.keysight.com/', # gives 403 error + r'^http://localhost', # ignore local servers +] + +# -- Custom CSS ------------------------------------------------------------- + +def setup(app): + app.add_css_file("custom.css") diff --git a/docs/contact.rst b/docs/source/contact.rst similarity index 100% rename from docs/contact.rst rename to docs/source/contact.rst diff --git a/docs/source/contributions.rst b/docs/source/contributions.rst new file mode 100644 index 000000000..a20b6807c --- /dev/null +++ b/docs/source/contributions.rst @@ -0,0 +1,34 @@ +=========================== +Community contributions +=========================== + +Extensions & Customization +=========================== + +- **QCoDeS Driver**: Save instrument configuration (`QCoDeS-QICK `_) +- **QICK-DAWG**: NV centers and quantum defects (`GitHub `_) +- **SpinQICK**: Solid-state spin qubits (`GitHub `_) + +Example code from the community +================================= + +.. note:: + List originally contributed by `Jay Chien `_ + (`PR #412 `_). + +Beyond the tutorials in :doc:`../tutorials/README`, QICK collaborators have shared +larger example codebases built on top of QICK. These are useful as reference +for structuring real measurement code, but are maintained independently of +this repository and are not guaranteed to track the latest QICK release. + +Built on tProc v1 +----------------- + +* `IEEE Quantum Week 2023 QICK tutorial `_ +* `Connie Miao, Schuster Lab `_ +* `Chao Zhou, Hatlab `_ + +Built on tProc v2 +----------------- + +* `Jay Chien, SQL Lab `_ diff --git a/docs/source/firmware/generators/index.rst b/docs/source/firmware/generators/index.rst new file mode 100644 index 000000000..f0b3ee9b2 --- /dev/null +++ b/docs/source/firmware/generators/index.rst @@ -0,0 +1,117 @@ +Signal Generators - QICK Firmware +================================== + +.. contents:: + :local: + :depth: 2 + +.. toctree:: + :maxdepth: 2 + :caption: 7. Signal Generators + :hidden: + + sg_v6 + sg_mux8 + sg_mixmux8 + sg_int4_v2 + sg_v4 + +Overview +-------- + +QICK ships several signal-generator IP cores. They all connect to the +tProcessor the same way (one AXI-Stream waveform port per channel, driven +by ``WPORT_WR``/:meth:`.QickProgram.pulse`) and the same way to a DAC, but +they trade off differently between **flexibility** (arbitrary pulse shapes) +and **simultaneity** (how many tones at once). Picking the right one for a +channel is a hardware/firmware decision (which core is instantiated on that +DAC channel in your board's bitstream) -- as a user you mostly just need to +know which kind of channel you have and how to talk to it, both covered +below. + +.. list-table:: Generator family at a glance + :header-rows: 1 + :widths: 22 15 15 48 + + * - Core + - Tones + - Envelope + - Typical use + * - :doc:`sg_v6` + - 1 (arbitrary shape) + - Yes, full rate + - The default choice for shaped pulses (Gaussian, DRAG, flat-top) -- + qubit drive, readout drive. + * - :doc:`sg_int4_v2` + - 1 (arbitrary shape) + - Yes, 4x-interpolated + - Same use case as SG-v6 on boards/channels where this lighter-weight + core is instantiated instead; has a DAC-side mixer (``HAS_MIXER``). + * - :doc:`sg_v4` + - 1 (arbitrary shape) + - Yes, full rate + - Legacy predecessor to SG-v6; not used in any board design currently + in this repository. Documented for driver/backward-compatibility + reference, not for new designs. + * - :doc:`sg_mux8` + - up to 8 (fixed CW) + - No + - Playing several simultaneous fixed tones from one channel -- e.g. a + frequency comb, or driving several resonators/qubits at once without + one DAC channel per tone. + * - :doc:`sg_mixmux8` + - up to 8 (fixed CW) + - No + - Same as SG-Mux8, plus a shared DAC-side mixer so the whole comb can be + placed anywhere (including outside the first Nyquist zone) via one + ``mixer_freq``. + +Arbitrary-envelope vs. multiplexed: which do I need? +----------------------------------------------------- + +**Use an arbitrary-envelope generator** (SG-v6, SG-int4-v2, or the legacy +SG-v4) when you need to *shape* a pulse -- a Gaussian, a DRAG pulse, a +flat-top pulse with rounded edges, or any other waveform you can compute in +Python. These cores have envelope memory (BRAM) you upload I/Q samples into +with :meth:`.QickProgram.add_envelope`, and play back with an ``arb`` or +``flat_top`` pulse style. Only one tone plays at a time per channel. + +**Use a multiplexed generator** (SG-Mux8 or SG-MixMux8) when you need +*several simultaneous CW tones* from one DAC channel and don't need to +shape each tone's envelope -- only its amplitude, phase, and which subset +of tones is on for how long. This is the common case for a frequency comb +or for driving multiple fixed resonances (e.g. several readout resonators) +through a single physical channel. Pick SG-MixMux8 over plain SG-Mux8 if +you need the comb somewhere the DAC's un-mixed first Nyquist zone can't +reach on its own. + +If you need *both* -- multiple simultaneous tones *and* per-tone arbitrary +shaping -- no single QICK generator core does that; you need one +arbitrary-envelope channel per shaped tone. + +Common Python workflow +----------------------- + +Every generator in this family is configured the same way, regardless of +which core is behind a given channel: declare it once with +:meth:`.QickProgram.declare_gen`, define named pulses with +:meth:`.QickProgram.add_pulse` (and :meth:`.QickProgram.add_envelope` first, +if it needs a shape), then play them with :meth:`.QickProgram.pulse`. See +:doc:`sg_v6`'s "Python Usage" section for worked examples (single pulse, +shaped pulse, multiple queued pulses), and :doc:`sg_mixmux8`'s Python +section for the multiplexed-generator variant (``mux_freqs``/``mux_gains`` +instead of one ``freq``/``gain`` per pulse). + +Related Documentation +---------------------- + +* :doc:`sg_v6` - the flagship arbitrary-envelope generator (start here) +* :doc:`sg_int4_v2`, :doc:`sg_v4` - other single-tone arbitrary generators +* :doc:`sg_mux8`, :doc:`sg_mixmux8` - multiplexed (multi-tone) generators +* :doc:`../index` - which generator core is on which channel, on the + reference board +* :doc:`/topics/gen_config` - shared pulse configuration options + (``outsel``/``mode``/``stdysel``) +* :doc:`/topics/freq_matching` - keeping generator and readout frequencies + in sync +* :doc:`../readouts/index` - the readout-side counterpart to this page diff --git a/docs/source/firmware/generators/sg_int4_v2.rst b/docs/source/firmware/generators/sg_int4_v2.rst new file mode 100644 index 000000000..f12ec281a --- /dev/null +++ b/docs/source/firmware/generators/sg_int4_v2.rst @@ -0,0 +1,391 @@ +================================================================== +Interpolated Signal Generator v2 (axis_sg_int4_v2) - QICK Firmware +================================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_sg_int4_v2** is a single-tone, arbitrary-envelope waveform generator. +Unlike :doc:`sg_v6`, which runs its DDS and envelope memory at the DAC's +full parallel sample rate, ``axis_sg_int4_v2`` stores the envelope at 1/4 of +the output rate and reconstructs the missing samples with a **4x +interpolation FIR filter** -- hence "int4". The module lives in the ``qick`` +firmware repository under ``firmware/ip/axis_sg_int4_v2/`` and is exposed to +Python through ``qick.drivers.generator.AxisSgInt4V2``. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +Where "int4" comes from +^^^^^^^^^^^^^^^^^^^^^^^^ + +The Xilinx FIR Compiler instance ``fir_0`` inside ``signal_gen.v`` is +configured with ``Filter_Type = Interpolation`` and ``Interpolation_Rate = +4`` (read from ``firmware/ip/axis_sg_int4_v2/src/fir_0/fir_0.xci``). Each +cycle it consumes one complex envelope sample read from a single-rate BRAM +and produces **4** interpolated I/Q sample pairs, which are fed in parallel +to **4** DDS instances (``N_DDS = 4``, a ``localparam`` in +``axis_sg_int4_v2.v``). This matches the Python driver's own docstring for +the shared base class ``AbsIntSignalGen``: *"AXIS Signal Generator with +envelope x4 interpolation"* (``qick_lib/qick/drivers/generator.py``, and +``FS_INTERPOLATION = 4`` on that class -- though as of this reading that +attribute is defined but not actually read anywhere else in the driver +stack, so treat it as documentation rather than as something that changes +behavior). + +This is the key architectural difference from :doc:`sg_v6` (and the older +``axis_signal_gen_v4``/``v5``, referred to together as SG-v4/v5/v6 in the +Python driver): SG-v6 stores the envelope **pre-expanded**, with one BRAM +pair (real/imag) per parallel DDS lane (``N_DDS`` pairs, default 16), so +``maxlen = 2**N * N_DDS`` samples of table are actually stored. This int4 +generator instead keeps a **single** real BRAM and a **single** imag BRAM +(see ``signal_gen_top.v``, instances ``mem_real_i``/``mem_imag_i``) holding +only ``2**N`` raw samples, and lets the FIR do the x4 expansion on the fly -- +which is why the Python driver's ``AbsIntSignalGen._init_config`` computes +``self.cfg['maxlen'] = 2**env_n`` (no ``* n_dds`` factor), with the comment +*"Table is interpolated. Length is given only by parameter N."* Both designs +still emit ``N_DDS`` output samples per clock on ``m_axis_tdata_o``; int4 +just trades BRAM area for a FIR filter to get there from a quarter-rate +table. + +Like SG-v6, this generator is single-output (one complex tone per channel, +via its own DDS + envelope multiplier) -- it is not multiplexed like +``axis_sg_mux4_v1``, which drives several simultaneous tones from one +channel. + +.. figure:: /images/firmware/sg_int4_v2-blocks.svg + :align: center + :width: 80% + + Top-level block diagram. Unlike SG-v6, there's a single real/imag + ``bram_tdp`` pair at the raw (1/4-rate) table rate -- ``fir_0`` does the + x4 interpolation before the DDS mixer, and there's no output mux: this + core always runs the DDS + multiplier path (see the note below). + +.. note:: + + Unlike SG-v6, ``axis_sg_int4_v2`` has **no** ``GEN_DDS``/``ENVELOPE_TYPE`` + synthesis parameters: it always instantiates the DDS+multiplier path (no + baseband-only mode) and always builds separate real/imag BRAMs (no + real-only envelope mode). Confirmed by the absence of those parameters in + ``axis_sg_int4_v2.v``/``signal_gen_top.v``/``signal_gen.v``, in contrast + to ``firmware/ip/axis_signal_gen_v6/src/signal_gen_top.v`` which has both. + +Because the DDS only needs to synthesize the frequency band around the +interpolated (quarter-rate) envelope, this generator relies on the RFDC's +own on-chip mixer for final upconversion: ``AbsIntSignalGen.HAS_MIXER = +True`` (whereas SG-v4/v5/v6's ``AxisSignalGen.HAS_MIXER = False``). In +practice this means ``QickProgram.declare_gen()`` requires a ``mixer_freq`` +argument for this channel (``qick_lib/qick/qick_asm.py``, ``declare_gen``: +*"generator %d has a digital mixer, but no mixer_freq was defined"*), which +is not the case for SG-v6. + +Version note (v1 -> v2) +^^^^^^^^^^^^^^^^^^^^^^^^ + +A brief diff against the sibling ``firmware/ip/axis_sg_int4_v1/`` shows the +two share essentially the same RTL structure (same module names, same +FIR/interpolation-by-4 configuration, same descriptor field layout). The one +substantive change is DDS/phase resolution: v1's ``dds_compiler_0`` is +configured with ``Phase_Width = 16`` (``Frequency_Resolution = 3906.25``, +i.e. Hz-scale steps), while v2's is configured with ``Phase_Width = 32`` +(``Frequency_Resolution = 0.06``) -- a large increase in tuning resolution. +This propagates through the whole datapath: the ``freq``/``phase`` fields of +the waveform descriptor grow from 16 to 32 bits each, and the descriptor +itself grows from 85 bits (v1, ``s1_axis_tdata`` is ``[84:0]``) to 160 bits +(v2, ``s1_axis_tdata`` is ``[159:0]``) to carry the wider fields plus +padding. This is exactly mirrored in the Python driver: ``AxisSgInt4V1`` sets +``B_DDS = B_PHASE = 16`` ("Interpolated generator with 16-bit frequency and +phase"), ``AxisSgInt4V2`` sets ``B_DDS = B_PHASE = 32`` ("Interpolated +generator with 32-bit frequency and phase"). + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``N`` + - 12 (``axis_sg_int4_v2.v``) + - Envelope memory address width. Table depth is ``2**N`` raw (complex) + samples; ``signal_gen_top.v``/``signal_gen.v`` themselves default to + ``N = 16`` when instantiated standalone, but the top-level wrapper + overrides this to 12. Real deployments commonly override it further + per channel -- e.g. ``firmware/projects/qick_tprocv2_216_standard/ + bd_2023-1.tcl`` instantiates different ``axis_sg_int4_v2`` copies with + ``CONFIG.N`` values of 13 and 14 -- so always read the deployed value + from ``soc.cfg['gens'][i]['maxlen']`` rather than assuming a default. + * - ``N_DDS`` + - 4 (fixed) + - Number of parallel DDS lanes / interpolated output samples per clock. + Declared as a ``parameter`` on ``signal_gen_top``/``signal_gen``, but + the top-level ``axis_sg_int4_v2.v`` wrapper pins it to a + ``localparam`` value of 4 -- it is not exposed as a customizable IP + parameter the way SG-v6's ``N_DDS`` is. + +-------------------------------------------------------------------- +3. Datapath +-------------------------------------------------------------------- + +The pipeline, in order (all in ``signal_gen.v`` unless noted): + +1. **ctrl (``ctrl.sv``)** -- an FSM (states ``READ_ST``/``CNT_ST``) pops one + 160-bit descriptor from the waveform FIFO into ``fifo_dout_r``, then holds + its fields active for ``nsamp`` clock cycles (each cycle advances the + envelope read address by 1 and advances each of the 4 DDS phase + accumulators by ``pinc``, with per-lane phase offsets ``pinc*0..pinc*3`` + so the 4 parallel DDS outputs are phase-continuous with each other). When + the count expires, it either fetches the next queued descriptor or, if + ``mode`` = periodic, keeps replaying the current one. +2. **Envelope memory (``mem_real_i``/``mem_imag_i``, ``bram.v``/dual-port + BRAM in ``signal_gen_top.v``)** -- one real and one imag BRAM, ``N`` bits + of address, 16 bits of data each, read at 1 address per clock (write side + is in the ``s0_axis_aclk`` domain via ``data_writer``, read side in + ``aclk``). +3. **Interpolation FIR (``fir_0``)** -- Xilinx FIR Compiler, interpolation + filter, rate 4, cycle latency 11 (+1 registered output = 12 per the + comment in ``signal_gen.v``). Expands the 1 complex envelope sample/clock + into 4 (``env_dout``, ``N_DDS*32`` bits wide). +4. **DDS (``dds_compiler_0``, x4 instances)** -- Xilinx DDS Compiler, + ``Phase_Width = 32``, ``Output_Width = 16`` (both real and imag), latency + 10 cycles. +5. **Complex multiply** -- each of the 4 lanes multiplies its DDS output + (I, Q) against the corresponding interpolated envelope sample: + ``real = I_dds*I_env - Q_dds*Q_env``, ``imag = I_dds*Q_env + Q_dds*I_env``, + each 32-bit partial product truncated to the top 16 bits (a + round-to-nearest-ish scheme: the accumulator's bit 30 is duplicated into + bit 31 before truncation, i.e. ``{p[30], p[30:0]}``, then bits ``[31:16]`` + are kept). This 16-bit result is the "product" ``outsel``. +6. **Output source mux (``src_la``)** -- per lane, selects between the + product (0), the raw DDS output (1), the raw interpolated complex + envelope sample -- both real and imag, unlike the readout's "input" + mode which is real-only -- for ``outsel = 2`` + (``{env_imag_la[i], env_real_la[i]}``), or an all-zero word (3, the + implicit default of the mux). +7. **Gain multiply + rounding** -- the muxed sample is multiplied by a + 16-bit signed ``gain`` register (latency-aligned via a 19-cycle + ``latency_reg``), and the 32-bit product is rounded down to 16 bits per + component (``prodg_y_full_*_r[30 -: 16]``, i.e. keep bits 30 down to 15 -- + a right-shift-by-15 round rather than a symmetric round-to-nearest). +8. **Output register** -- while the waveform is active (``en_la_r``), the + rounded, gain-scaled sample (``round_r``) is driven on + ``m_axis_tdata_o``. Once inactive, the output holds the value selected by + ``stdysel``: the last active sample (``stdysel = "last"``, code 0 -- and + specifically the 4th of the last active clock's 4 lanes, broadcast to all + 4 output lanes, since ``last_r[i] <= round[N_DDS-1]`` for every ``i``) or + all zeros (``stdysel = "zero"``, code 1). + +.. note:: + + ``params2pulse()`` in ``qick_lib/qick/asm_v2.py`` has an explicit + workaround specific to this generator family (``axis_sg_int4_v1`` and + ``axis_sg_int4_v2``, but not SG-v6): for ``flat_top`` pulses, an extra + zero-gain, minimum-length ``"dds"``-outsel waveform is appended after the + ramp-down, with the comment *"workaround for FIR bug: we play a + zero-gain min-length DDS pulse after the ramp-down, which brings the FIR + to zero."* This is presumably needed because the interpolation FIR (step + 3 above) has state/history that would otherwise bleed into the following + pulse; SG-v6 has no FIR in its envelope path and needs no such + workaround. + +-------------------------------------------------------------------- +4. Waveform Descriptor (S1_AXIS) +-------------------------------------------------------------------- + +``s1_axis_tdata`` is a 160-bit word pushed into a 160-bit-wide, 16-entry +FIFO (``fifo_i`` in ``signal_gen_top.v``, same depth as SG-v6's). Field +layout, read directly from the header comment and bit-slice assignments in +``ctrl.sv``: + +.. list-table:: Waveform descriptor fields + :header-rows: 1 + :widths: 15 15 15 55 + + * - Field + - Bits + - Width + - Description + * - ``freq`` + - ``[31:0]`` + - 32 + - DDS frequency tuning word for all 4 lanes (phase increment per + clock, before the x4 per-lane phase offset). + * - ``phase`` + - ``[63:32]`` + - 32 + - DDS phase offset. + * - ``addr`` + - ``[79:64]`` + - 16 + - Start address of the envelope in ``mem_real``/``mem_imag``. + * - (unused) + - ``[95:80]`` + - 16 + - Not read by ``ctrl.sv``. + * - ``gain`` + - ``[111:96]`` + - 16 + - Signed output gain. + * - (unused) + - ``[127:112]`` + - 16 + - Not read by ``ctrl.sv``. + * - ``nsamp`` + - ``[143:128]`` + - 16 + - Number of clock cycles (each emitting ``N_DDS`` = 4 output samples) + the descriptor stays active in one-shot mode. + * - ``outsel`` + - ``[145:144]`` + - 2 + - Output source select: 0 product, 1 dds, 2 input (envelope), 3 zero. + * - ``mode`` + - 146 + - 1 + - 0 one-shot (stop after ``nsamp``), 1 periodic (replay). + * - ``stdysel`` + - 147 + - 1 + - 0 last (hold last sample), 1 zero. + * - ``phrst`` + - 148 + - 1 + - Reset the DDS phase accumulator when this descriptor loads. + * - (unused) + - ``[159:149]`` + - 11 + - Not read by ``ctrl.sv``. + +This layout is bit-for-bit identical to SG-v6's 160-bit descriptor (same +field widths, same bit positions -- compare the header comment in this +IP's ``ctrl.sv`` against ``firmware/ip/axis_signal_gen_v6/src/ctrl.sv``). +Confirmed in the routing firmware itself: in +``firmware/projects/qick_tprocv2_216_standard/bd_2023-1.tcl``, the +``axis_sg_int4_v2_0`` instance's ``s1_axis`` is wired to +``sg_translator_5/m_gen_v6_axis`` -- i.e. it uses the **same** +``sg_translator`` output path (``OUT_TYPE = 0``, "gen_v6") as SG-v6, not the +dedicated, 88-bit, 16-bit-freq/phase ``int4_v1`` translation path +(``OUT_TYPE = 1``) that ``axis_sg_int4_v1`` uses. ``sg_translator`` +(``firmware/ip/qick_sg_translator/src/sg_translator.v``) is the block that +sits between the tProcessor's generic 168-bit wave-memory word (``freq``, +``phase``, ``env``, ``gain``, ``length``, ``conf``) and this generator's +160-bit ``s1_axis`` -- it truncates the tProc's 32-bit ``length`` field down +to the 16-bit ``nsamp`` here, but passes ``freq``/``phase`` through +unmodified at full 32-bit width (unlike the truncation ``int4_v1`` needs). + +-------------------------------------------------------------------- +5. Register Map (S_AXI, AXI-Lite) +-------------------------------------------------------------------- + +``axi_slv.vhd`` implements a 32-bit AXI-Lite slave with a 6-bit address bus +(16 possible word-aligned registers), of which only two are wired to the +rest of the IP: + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Register + - Offset + - Width + - Description + * - ``START_ADDR_REG`` + - 0x00 + - 32 bits + - Starting address in envelope memory for the next block of samples + written via ``S0_AXIS``. + * - ``WE_REG`` + - 0x04 + - bit 0 + - Envelope-memory write enable. The ``data_writer`` FSM + (``data_writer.vhd``) waits in ``INIT_ST`` until this is asserted, + latches ``START_ADDR_REG`` as the write pointer, then streams + ``S0_AXIS`` samples into ``mem_real``/``mem_imag`` (32-bit words: + bits ``[15:0]`` real, ``[31:16]`` imag), auto-incrementing the + address each valid beat. + +This matches ``AbsIntSignalGen.REGISTERS = {'start_addr_reg': 0, 'we_reg': +1}`` in the Python driver (note: these are *register indices*, i.e. +``we_reg`` is word offset 1 = byte offset 0x04, not bit 1 of word 0). + +-------------------------------------------------------------------- +6. Python Interface +-------------------------------------------------------------------- + +The class hierarchy (``qick_lib/qick/drivers/generator.py``): + +* ``AbsSignalGen`` -- common signal-gen config: ``MAXV``, ``set_nyquist()``, + ``set_mixer_freq()``/``get_mixer_freq()`` (only usable if ``HAS_MIXER``). +* ``AbsArbSignalGen(AbsSignalGen)`` -- adds the envelope-memory ``load()`` + method (DMA transfer + ``START_ADDR_REG``/``WE_REG`` pulsing), plus + ``SAMPS_PER_CLK`` (1 for this generator -- envelope samples are loaded + one per clock, unlike SG-v6's 16) and ``MAXV_SCALE``. +* ``AbsPulsedSignalGen(AbsSignalGen)`` -- adds tProcessor-port discovery + (``TPROC_PORT = 's1_axis'``) for tProc-triggered playback. +* ``AbsIntSignalGen(AbsArbSignalGen, AbsPulsedSignalGen)`` -- the shared + base for both int4 versions: ``HAS_MIXER = True``, + ``FS_INTERPOLATION = 4``, ``MAXV_SCALE = 0.9`` (reduced from the default + 1.0 "to prevent interpolation overshoot: the output of the interpolation + filter may exceed the max value of the input points," per the class + docstring), ``REGISTERS = {'start_addr_reg': 0, 'we_reg': 1}``, and + ``maxlen = 2**N`` (table is interpolated -- see Section 1). +* ``AxisSgInt4V2(AbsIntSignalGen)`` -- ``bindto = ['user.org:user: + axis_sg_int4_v2:1.0', 'QICK:QICK:axis_sg_int4_v2:1.0']``, + ``B_DDS = B_PHASE = 32``. + +Normal QICK programs don't touch these registers directly -- they go through +:meth:`.QickProgram.declare_gen` (to set Nyquist zone and, for this +generator, the required ``mixer_freq``) and +:meth:`.QickProgramV2.add_pulse` (style ``"arb"`` or ``"flat_top"``, using +an envelope loaded with :meth:`.QickProgram.add_envelope`). + +.. code-block:: python + :caption: Configuring and playing a pulse on an int4-v2 channel + + from qick import * + import numpy as np + + soc = QickSoc() + gen_ch = 0 # must be an axis_sg_int4_v2 channel, e.g. soc['gens'][gen_ch]['type'] == 'axis_sg_int4_v2' + + class MyProgram(QickProgramV2): + def _initialize(self, cfg): + # this generator has a mixer (HAS_MIXER=True), so mixer_freq is required + self.declare_gen(ch=gen_ch, nqz=1, mixer_freq=500.0, ro_ch=0) + + # envelope samples are int16; length is checked against the + # deployed maxlen = 2**N for this channel + n = 400 + sigma = n / 8 + env = 32767 * np.exp(-0.5 * ((np.arange(n) - n/2) / sigma)**2) + self.add_envelope(ch=gen_ch, name="gauss", idata=env.astype(np.int16)) + + self.add_pulse(ch=gen_ch, name="pulse1", style="arb", + freq=100.0, phase=0, gain=0.5, + envelope="gauss", outsel="product") + + def _body(self, cfg): + self.pulse(ch=gen_ch, name="pulse1", t=0) + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`sg_v6` -- SG-v6, the full-rate/full-BRAM sibling generator this + page repeatedly compares against. +* :doc:`/tprocv2_trm` -- tProcessor v2 sequencing (``WPORT_WR``/``add_pulse`` + path that ultimately fills this generator's ``S1_AXIS`` via + ``qick_sg_translator``). +* :doc:`../index` -- Firmware overview and channel assignments. +* :doc:`/topics/gen_config` -- ``outsel``/``mode``/``stdysel`` semantics + (shared across all pulsed generators in this doc set). +* :doc:`/topics/freq_matching` -- keeping generator and readout frequencies + in sync (relevant here because of the required ``mixer_freq``). +* `axis_sg_int4_v2 source code `_ diff --git a/docs/source/firmware/generators/sg_mixmux8.rst b/docs/source/firmware/generators/sg_mixmux8.rst new file mode 100644 index 000000000..da90121cb --- /dev/null +++ b/docs/source/firmware/generators/sg_mixmux8.rst @@ -0,0 +1,368 @@ +============================================================================== +Multiplexed Signal Generator with Mixer (axis_sg_mixmux8_v1) - QICK Firmware +============================================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_sg_mixmux8_v1** is an 8-tone multiplexed (muxed) signal generator, +almost identical in its control logic to :doc:`sg_mux8` +(``axis_sg_mux8_v1``), but with a **complex (I/Q) datapath** so that its +output can drive the RFDC's own digital mixer/NCO. The module lives under +``firmware/ip/axis_sg_mixmux8_v1/`` and is exposed to Python through +``qick.drivers.generator.AxisSgMixMux8V1``. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +Like ``axis_sg_mux8_v1``, this block plays up to 8 independent, phase-coherent +CW tones simultaneously: each tone has its own frequency (``PINCx_REG``), +phase (``POFFx_REG``) and gain (``GAINx_REG``), and the tProcessor selects +which subset of the 8 tones is active for a given pulse via an 8-bit mask +and a sample count, pushed onto ``s_axis`` (see Section 3.2 below). The +8 (masked) tones are summed together and streamed out at up to ``N_DDS`` +samples per clock. + +The one substantive RTL difference from ``axis_sg_mux8_v1`` -- confirmed by +diffing the two IPs' ``src/`` directories file-by-file (``diff -rq +firmware/ip/axis_sg_mux8_v1/src firmware/ip/axis_sg_mixmux8_v1/src``) -- is +that **every per-tone DDS core is configured as a complex (I/Q) oscillator +instead of a real-valued one**, and the entire summation/quantization +datapath downstream of it is duplicated to carry both components. Concretely: + +* In ``dds_top.v``'s ``dds_compiler_0`` (the Xilinx DDS Compiler core), the + ``Output_Selection`` parameter is ``"Sine"`` in ``axis_sg_mux8_v1`` (a + real-valued 16-bit output) versus ``"Sine_and_Cosine"`` in + ``axis_sg_mixmux8_v1`` (a packed 32-bit ``{cosine, sine}`` output). This is + the actual source of the "mix" in the name. +* ``dds_dout_o``, the DDS-to-gain-stage bus, is ``N_DDS*16`` bits wide in + ``axis_sg_mux8_v1`` and ``N_DDS*32`` bits wide in ``axis_sg_mixmux8_v1`` + (16-bit real vs. 16-bit-I + 16-bit-Q per DDS lane). +* The per-tone gain multiply in ``dds_top.v`` (``prod_a_real = dds_dout_la[i]`` + in mux8 vs. ``prod_a_real = dds_dout_la[i][15:0]`` / + ``prod_a_imag = dds_dout_la[i][31:16]`` in mixmux8) is duplicated: the + *same* signed 16-bit ``GAIN_REG`` value is multiplied independently against + the real and imaginary DDS samples. This is a real (scalar) amplitude + scale of a complex sample, **not** a complex/quadrature multiply -- there + is no cross term (no ``I*sin(LO) - Q*cos(LO)``-style rotation) anywhere in + this IP's RTL. +* In ``sg_mux8.v``, the 8-tone masking, the 7-stage pairwise adder tree that + sums the (up to 8) active tones per DDS lane, and the mask-popcount-based + output quantization (see Section 3.3) are all bit-for-bit the same logic + as ``axis_sg_mux8_v1``'s -- just instantiated twice, once for the real + (I) rail and once for the imaginary (Q) rail, in parallel. ``m_axis_tdata_o`` + ends up ``N_DDS*32`` bits wide (16-bit I + 16-bit Q per DDS lane) instead of + ``axis_sg_mux8_v1``'s ``N_DDS*16`` bits (real only). +* ``axi_slv.v`` (the AXI-Lite register file), ``ctrl.sv`` (the FIFO-drain / + mask / enable FSM), ``phase_ctrl.sv`` (the phase accumulator, including its + use of ``mult_32x32.v``), ``latency_reg.v`` and ``synchronizer_n.vhd`` are + **byte-identical files** between the two IPs (confirmed by ``diff -rq`` + reporting no difference for any of them). There is **no additional + register, FSM, or signal path in this IP's own RTL for an internal mixer** + -- no ``MIXFREQ``-style register, no second NCO, no per-lane local + oscillator anywhere in ``axis_sg_mixmux8_v1``'s source. + +So where is the "mixer"? It is the RFDC's (RF Data Converter) own digital +mixer/NCO on the DAC tile, external to this IP -- ``axis_sg_mixmux8_v1`` +simply produces the complex (I/Q) samples that RFDC mixer needs as input. +This is corroborated by the Python driver (``qick_lib/qick/drivers/generator.py``): + +* ``AxisSgMux8V1``'s docstring: *"AXIS Signal Generator with 8 muxed outputs, + fullspeed (**no DAC mixer**)."* versus ``AxisSgMixMux8V1``'s: *"AXIS Signal + Generator with 8 muxed outputs, **using DAC mixer**."* +* ``AbsSignalGen.set_mixer_freq()``/``get_mixer_freq()`` (the methods that + back this behavior for every ``HAS_MIXER=True`` generator, including this + one) call straight through to ``self.rf.set_mixer_freq(self['dac'], f, + ...)`` -- i.e. they configure the RFDC block driver, not any register + inside ``axis_sg_mixmux8_v1`` itself. +* ``AxisConstantIQ`` (another ``HAS_MIXER=True`` generator) states the same + architecture explicitly: *"Plays a constant IQ value, which gets mixed + with the DAC's built-in oscillator."* + +Because the mixer frequency is a single value set on the DAC tile +(``set_mixer_freq(ch, f)`` takes one ``f`` for the whole generator channel, +and ``QickConfig.calc_muxgen_regs()`` computes every tone's ``PINC`` as +``f_dds = freq - mixer_freq`` relative to that one value), **the local +oscillator is shared across all 8 muxed tones on a channel, not independent +per tone/lane**. The 8 tones are combined digitally at baseband (each +keeping its own ``PINC``/``POFF``/``GAIN`` relative to that shared center +frequency) and the RFDC mixer then shifts the *entire already-summed comb* +up (or down) in frequency, which is what lets ``axis_sg_mixmux8_v1`` +(unlike ``axis_sg_mux8_v1``) place its whole multi-tone output away from DC, +including outside the DAC's first Nyquist zone. + +.. figure:: /images/firmware/sg_mixmux8-blocks.svg + :align: center + :width: 85% + + Top-level block diagram -- identical control/FIFO structure to + ``axis_sg_mux8_v1``, but each ``dds_top_mixmux8`` tone slot is a + complex (I/Q) oscillator and the adder tree is duplicated for both + rails. ``M_AXIS`` carries the already-summed I/Q comb to the RFDC's + own mixer on the DAC tile -- there is no mixer inside this IP. + +.. note:: + + This page documents only ``axis_sg_mixmux8_v1``'s own RTL and driver. + The RFDC mixer/NCO itself (register layout, valid frequency ranges, + Nyquist-zone handling) lives in AMD's RF Data Converter IP and the + ``qick.qick.AxisRFDC``-family driver, not in this IP -- see + :doc:`/topics/freq_matching` for the software-level picture of how + generator and readout frequencies are kept consistent. + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``N_DDS`` + - 2 + - Number of parallel DDS/output lanes (samples produced per clock on + ``m_axis_tdata_o``). Each of the 8 tone generators instantiates + ``N_DDS`` parallel ``dds_compiler_0`` cores; the 8 tones are then + summed lane-by-lane. Real board builds typically use a larger value + than the RTL default -- the Python driver stores it as ``gen.NDDS`` + (read from the block's HWH parameters) but does **not** publish it in + ``soc['gens'][i]``, so read ``soc.gens[i].NDDS`` directly if you need + it at runtime. + +There is no synthesis parameter that changes the mixer/complex behavior -- +that is hard-wired into the ``dds_compiler_0`` IP core configuration +(``Output_Selection = "Sine_and_Cosine"``), unlike ``ENVELOPE_TYPE`` or +``GEN_DDS`` on the arbitrary-waveform generators (:doc:`sg_v6`). + +-------------------------------------------------------------------- +3. Datapath +-------------------------------------------------------------------- + +3.1 Top-level structure +^^^^^^^^^^^^^^^^^^^^^^^ + +``axis_sg_mixmux8_v1.v`` instantiates exactly two blocks (this wiring is +identical in shape to ``axis_sg_mux8_v1.v``, differing only in the +``m_axis_tdata`` bus width): + +* ``axi_slv`` -- the AXI-Lite register file (Section 4). +* ``sg_mux8`` -- the FIFO, control FSM, 8 DDS/gain stages, adder tree and + quantizer (Sections 3.2-3.3). + +3.2 Waveform trigger FIFO and control FSM +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The tProcessor triggers playback by pushing a 40-bit descriptor onto +``s_axis_tdata_i`` (via a 16-deep FIFO, ``fifo_wr_en = s_axis_tvalid_i``): + +.. list-table:: Waveform-trigger descriptor (``s_axis_tdata_i``, 40 bits) + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Bits + - Description + * - ``mask`` + - ``[39:32]`` (8 bits) + - One bit per tone (bit *i* enables tone *i*'s ``GAINi``/``PINCi``/ + ``POFFi`` in the sum for this playback burst). + * - ``nsamp`` + - ``[31:0]`` + - Number of output samples (cycles) to play before returning to idle. + +``ctrl.sv`` is a small FSM (``READ_ST -> CNT0_ST -> CNT_ST -> READ_ST``) +that drains this FIFO one descriptor at a time, latches ``mask``/``nsamp``, +asserts the shared output-enable (``en_o``, latency-matched to the datapath +via a ``latency_reg``) for ``nsamp`` cycles, and holds ``mask_o`` for the +duration. This file is byte-identical between ``axis_sg_mux8_v1`` and +``axis_sg_mixmux8_v1``. + +3.3 Per-tone DDS, gain, and the adder/quantizer tree +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``sg_mux8.v`` instantiates 8 ``dds_top`` blocks (one per tone), each driven +by that tone's ``PINCx_REG``/``POFFx_REG``/``GAINx_REG``. Inside +``dds_top.v``: + +1. ``phase_ctrl`` computes ``N_DDS`` parallel, phase-coherent phase/PINC + words (using ``mult_32x32`` for the ``PINC*N_DDS`` and + ``PINC*sample_count`` products needed to keep the ``N_DDS`` lanes + phase-coherent), latched on a rising edge of the (resynchronized) + ``WE_REG``. +2. Each lane's ``dds_compiler_0`` produces one DDS sample per clock -- + real-only (``Sine``) in ``axis_sg_mux8_v1``, complex I/Q + (``Sine_and_Cosine``) in ``axis_sg_mixmux8_v1``. +3. The (real, or real+imag) DDS sample(s) are scaled by that tone's signed + 16-bit ``GAIN_REG`` (``prod_real = I*gain``, and in mixmux8 also + ``prod_imag = Q*gain``), each independently rounded back down to 16 bits. + +Back in ``sg_mux8.v``, for each DDS lane the (up to 8) tones selected by +``mask`` are summed via a 7-stage pairwise binary adder tree +(``add0..add7 -> sum0..sum3 -> sum4..sum5 -> sum6``), growing from 16 to 19 +signed bits to avoid overflow, then quantized back down based on +``qsel = popcount(mask)`` (the number of simultaneously active tones): + +.. list-table:: Output quantization vs. number of active tones + :header-rows: 1 + :widths: 20 20 60 + + * - ``qsel`` (active tones) + - Shift + - Bits taken from the 19-bit ``sum6`` + * - 1 + - none + - ``[15:0]`` + * - 2 + - ``>>1`` + - ``[16:1]`` + * - 3 or 4 + - ``>>2`` + - ``[17:2]`` + * - 5, 6, 7 or 8 + - ``>>3`` + - ``[18:3]`` + +This scaling (average rather than a raw sum) is applied identically and +independently to the real and imaginary sums in ``axis_sg_mixmux8_v1``; in +``axis_sg_mux8_v1`` it is applied only to the single real sum. The final +per-DDS-lane output word on ``m_axis_tdata_o`` is +``{dout_imag[i], dout_real[i]}`` (32 bits: Q in the high 16 bits, I in the +low 16) in ``axis_sg_mixmux8_v1``, versus just ``dout_real[i]`` (16 bits) in +``axis_sg_mux8_v1``. + +-------------------------------------------------------------------- +4. Register Map +-------------------------------------------------------------------- + +The AXI-Lite register file (``axi_slv.v``, byte-identical to +``axis_sg_mux8_v1``'s) is a flat array of 32-bit words, word-addressed from +``s_axi_awaddr[7:2]``. Verified directly from the RTL's address-decode +``case`` statement and its register->port ``assign`` list at the bottom of +``axi_slv.v``: + +.. list-table:: + :header-rows: 1 + :widths: 20 15 15 50 + + * - Register + - Byte address + - Width + - Description + * - ``PINC0_REG`` .. ``PINC7_REG`` + - ``0x00`` .. ``0x1C`` + - 32 bits + - DDS frequency tuning word for tone 0..7 (``B_DDS = 32`` on the + driver). + * - ``POFF0_REG`` .. ``POFF7_REG`` + - ``0x20`` .. ``0x3C`` + - 32 bits + - DDS phase offset for tone 0..7 (``B_PHASE = 32`` on the driver). + * - ``GAIN0_REG`` .. ``GAIN7_REG`` + - ``0x40`` .. ``0x5C`` + - 32 bits (AXI word); **only bits [15:0] reach the hardware** + - Signed gain for tone 0..7. The AXI-Lite word is 32 bits wide, but in + both ``axis_sg_mux8_v1.v`` and ``axis_sg_mixmux8_v1.v`` the + ``GAINx_REG`` wire connecting ``axi_slv`` to ``sg_mux8``/``dds_top`` + is declared ``[15:0]`` -- Verilog silently truncates to the low 16 + bits, so only a signed 16-bit gain actually reaches the multiplier. + In practice this is not observable from software, since the driver's + ``MAXV = 2**15-2`` keeps gain values within signed-16-bit range + anyway -- but it means the register is *not* a full 32-bit gain + field, despite being AXI-mapped as one. + * - ``WE_REG`` + - ``0x60`` + - 1 bit (bit 0 of the word; pulse) + - Write-enable strobe. A 0->1 transition (resynchronized per-DDS by + ``synchronizer_n`` inside ``phase_ctrl``) latches the *current* + values of all 8 ``PINCx``/``POFFx``/``GAINx`` registers into the + datapath atomically -- writing individual ``PINCx``/``POFFx``/ + ``GAINx`` registers alone has no effect on the generator's output + until ``WE_REG`` is pulsed. + +This matches, word for word, the register map that +``AbsMuxSignalGen._init_config()`` builds programmatically in the Python +driver (``pinc0..7_reg`` at word indices 0-7, ``poff0..7_reg`` at 8-15, +``gain0..7_reg`` at 16-23, ``we_reg`` at 24) -- no discrepancy was found +between the RTL address decode and the driver's register offsets. + +-------------------------------------------------------------------- +5. Python Interface +-------------------------------------------------------------------- + +``AxisSgMixMux8V1`` (``qick_lib/qick/drivers/generator.py``) is a thin +subclass of ``AbsMuxSignalGen`` (shared with ``AxisSgMux8V1`` and the other +muxed generators) that only sets the class attributes describing this IP's +capabilities: + +.. code-block:: python + + class AxisSgMixMux8V1(AbsMuxSignalGen): + bindto = ['user.org:user:axis_sg_mixmux8_v1:1.0', + 'QICK:QICK:axis_sg_mixmux8_v1:1.0'] + HAS_MIXER = True + B_DDS = 32 + N_TONES = 8 + HAS_GAIN = True + HAS_PHASE = True + B_PHASE = 32 + +The only difference from ``AxisSgMux8V1``'s class attributes is +``HAS_MIXER = True`` (vs. ``False``) -- everything else (tone count, +frequency/phase/gain widths) is the same. + +As with the other pulsed generators, most QICK programs never call the +low-level register-setting methods directly -- they configure this +generator through :meth:`.QickProgram.declare_gen` and then play tones by +selecting a subset of them with ``mask``: + +.. code-block:: python + :caption: Configuring and playing tones on axis_sg_mixmux8_v1 + + from qick import * + + soc = QickSoc() + prog = QickProgram(soc) # or QickProgramV2, depending on tProc version + + # gen_ch 0 is bound to an axis_sg_mixmux8_v1 instance in this example. + # Because HAS_MIXER=True, a mixer_freq is required; it sets the RFDC's + # NCO, shared by all 8 tones on this channel. + prog.declare_gen( + ch=0, + nqz=1, + mixer_freq=500.0, # MHz, shared LO for all 8 tones + mux_freqs=[-50.0, 0.0, 75.0], # MHz, offsets from mixer_freq + mux_gains=[0.9, 1.0, 0.5], + mux_phases=[0, 0, 90], + ro_ch=0, # frequency-match to readout 0 + ) + + # Play tones 0 and 2 (mask selects which declared tones sum together) + prog.pulse(ch=0, style='const', mask=[0, 2], length=1000) + +Directly poking registers (``set_tones()``/``set_tones_int()`` on the +driver, reached as ``soc.gens[0]``) is supported for debugging but is not +the normal path -- see ``AbsMuxSignalGen.set_tones()``'s docstring: *"This +method is not normally used, it's only for debugging and testing."* Setting +the mixer frequency directly (``soc.gens[0].set_mixer_freq(f)``) is likewise +meant for constant-IQ-style outputs; for tProc-controlled generators like +this one, :meth:`.QickProgram.config_gens` calls it for you from the +``mixer_freq`` passed to ``declare_gen()``. + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`sg_mux8` -- the real-valued (no DAC mixer) counterpart to this IP; + read that page first for the parts of the datapath (FIFO, control FSM, + adder tree, quantization) that are shared verbatim. +* :doc:`sg_v6` -- the arbitrary-envelope (non-muxed) signal generator, for + comparison with a different generator family. +* :doc:`/topics/freq_matching` -- keeping generator and readout frequencies + (including the shared mixer frequency) consistent. +* :doc:`../readouts/index` -- readout system overview, including the readout-side + digital mixer that this generator's frequency is often matched against. diff --git a/docs/source/firmware/generators/sg_mux8.rst b/docs/source/firmware/generators/sg_mux8.rst new file mode 100644 index 000000000..f6425e23d --- /dev/null +++ b/docs/source/firmware/generators/sg_mux8.rst @@ -0,0 +1,345 @@ +======================================================== +Signal Generator Mux8 (axis_sg_mux8_v1) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_sg_mux8_v1** is a fixed-tone, 8-way multiplexed signal generator: it +plays up to 8 simultaneous, independently-configured DDS tones per channel, +as opposed to the single-tone arbitrary generators (:doc:`sg_v6` and its +siblings) which play one tone at a time but can shape it with an arbitrary +envelope. There is no envelope memory here -- a "pulse" on this generator is +just "play some subset of the 8 pre-programmed tones for N samples". The +module lives in the ``qick`` firmware repository under +``firmware/ip/axis_sg_mux8_v1/`` and is exposed to Python through +``qick.drivers.generator.AxisSgMux8V1``. + +A related core, **axis_sg_mixmux8_v1**, adds a DAC-side digital mixer on top +of the same 8-tone architecture; it is documented separately and is not +covered further here. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +``axis_sg_mux8_v1`` (top module ``axis_sg_mux8_v1.v``, core datapath +``sg_mux8.v``) instantiates 8 independent tone slots (``N_OUT = 8``, a +``localparam`` in ``sg_mux8.v`` -- not exposed as a synthesis parameter, so +changing the tone count requires editing the RTL). Each slot has its own +frequency (``PINCx_REG``), phase (``POFFx_REG``) and gain (``GAINx_REG``) +register, all configured once over AXI-Lite. Unlike a pulse on an arbitrary +generator, a "pulse" here does not carry frequency/phase/gain -- those are +static per-tone settings -- it only selects *which* of the 8 tones are +audible (an 8-bit ``mask``) and *for how many samples*, streamed in over +``s_axis`` from the tProcessor. + +Each tone slot is built from: + +* **phase_ctrl.sv** -- a phase-coherent NCO controller (see + :ref:`sgmux8-phase-coherence` below). +* ``N_DDS`` parallel instances of the Xilinx DDS Compiler IP + (``dds_compiler_0``, one phase accumulator/sine table per parallel sample + lane -- see :ref:`sgmux8-synth-params`). +* A signed 16x16 multiply against that tone's ``GAINx_REG``, applied + identically to all ``N_DDS`` parallel-lane samples of the tone. + +The 8 tones' (gain-scaled) samples are then summed lane-by-lane across all +``N_DDS`` parallel output positions and requantized back to 16 bits (see +:ref:`sgmux8-summer`) to produce the final ``m_axis`` output. + +**Compared to the mux4 generators** (``AxisSgMux4V1``/``V2``/``V3``, driver +classes in ``qick.drivers.generator``): those support only 4 simultaneous +tones rather than 8, and (for V1/V2) drive a DAC-side digital mixer +(``HAS_MIXER = True``) that this core does not have +(``AxisSgMux8V1.HAS_MIXER = False`` -- see the Python Interface section). +The mux4 RTL is out of scope for this document. + +.. figure:: /images/firmware/sg_mux8-blocks.svg + :align: center + :width: 85% + + Top-level block diagram. The 8-bit mask and sample count arrive over + ``S_AXIS`` and are queued in ``fifo_sync``; ``ctrl_sgmux8`` pops one + descriptor at a time and gates the 8 ``dds_top_sgmux8`` tone slots + (each internally phase-coherent NCO + DDS + gain) with mask/en. The + summed, requantized output feeds ``M_AXIS``. + +.. _sgmux8-synth-params: + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``N_DDS`` + - 2 + - Number of parallel DDS/sample lanes per tone, i.e. how many output + samples each tone (and the final summed waveform) produces per + ``aclk`` cycle. ``m_axis_tdata`` is ``N_DDS*16`` bits wide (16 bits + per sample lane). Real board bitstreams use larger values -- e.g. the + ZCU216 tProc-v2 reference design (``firmware/projects/qick_tprocv2_216_demo``) + instantiates this core with ``N_DDS = 16``. + +The number of simultaneous tones (``N_OUT = 8``) is a fixed ``localparam`` +in ``sg_mux8.v``, not a synthesis parameter -- there is no generic to change +the tone count without editing the RTL. + +The per-tone DDS core (``dds_compiler_0``, Xilinx DDS Compiler) is +configured (per its ``.xci``) with 32-bit phase width, 16-bit output width, +``Output_Selection = Sine`` (a single real output per lane, not sin+cos), +and a fixed pipeline latency of 10 cycles. + +-------------------------------------------------------------------- +3. Datapath +-------------------------------------------------------------------- + +.. _sgmux8-phase-coherence: + +3.1 Per-Tone Frequency/Phase Generation (``phase_ctrl.sv``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each tone's phase is *not* computed by a simple running accumulator that +adds ``PINC`` every cycle. Instead ``phase_ctrl.sv`` keeps a free-running, +never-reset 32-bit sample counter ``cnt_n`` (incremented by ``N_DDS`` every +``aclk`` cycle) and computes each parallel lane's absolute phase directly +from it: + +.. code-block:: text + + phase[lane i] = (cnt_n + i) * PINC + POFF (mod 2^32) + +(implemented as ``pinc*cnt_n`` via a dedicated 32x32 pipelined multiplier, +``mult_32x32.v``, plus ``pinc*i`` for the within-cycle lane offset). Because +phase is recomputed from an absolute, ever-incrementing counter rather than +accumulated incrementally, the tone stays phase-coherent across +configuration changes -- there's no accumulator to reset or discontinue. + +``PINC_REG``/``POFF_REG`` updates are staged: the AXI-Lite-written values +only take effect when ``WE_REG`` is pulsed 0->1->0 (resynchronized into the +``aclk`` domain by a ``synchronizer_n``). Notably, **``GAIN_REG`` is *not* +gated by ``WE_REG``** -- in ``dds_top.v`` the gain multiplicand is a plain +continuous assignment (``assign gain = GAIN_REG;``), so a gain write takes +effect on its own timing rather than being staged together with the next +``WE_REG`` pulse like frequency/phase are. In normal driver usage (see +Python Interface) this is not observable, since ``PINCx``/``POFFx``/``GAINx`` +are all written before the single ``WE_REG`` pulse that follows -- but it is +a real asymmetry in the RTL, worth knowing if you ever write registers by +hand and expect all three to update atomically together. + +.. _sgmux8-summer: + +3.2 Gain Scaling and 8-Tone Summation (``sg_mux8.v``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Inside ``dds_top.v``, each tone's 16-bit signed DDS sample is multiplied by +that tone's 16-bit signed gain and requantized to 16 bits (bits ``[30:15]`` +of the 32-bit product -- a Q15 scale, consistent with the driver's +``MAXV = 2**15-2`` gain convention). + +The 8 tones are masked (a disabled tone's contribution is forced to +``16'h0000``, not just zero-gained) and then summed lane-by-lane (across +each of the ``N_DDS`` parallel sample positions) through a pipelined, +3-stage binary adder tree (8 -> 4 -> 2 -> 1), growing to a 19-bit signed +intermediate to avoid overflow when all 8 tones are active simultaneously. +The final 19-bit sum is then requantized back down to 16 bits, with the +right-shift amount chosen by ``qsel`` -- the number of tones currently +enabled in the mask (a popcount of the 8 mask bits): + +.. list-table:: Output requantization shift (``sg_mux8.v``) + :header-rows: 1 + :widths: 20 20 60 + + * - Active tones (``qsel``) + - Shift applied + - Notes + * - 1 + - none (bits ``[15:0]``) + - + * - 2 + - >> 1 (bits ``[16:1]``) + - + * - 3 or 4 + - >> 2 (bits ``[17:2]``) + - Same shift for 3 and 4 tones -- not an exact per-count division. + * - 5, 6, 7, 8 (or 0) + - >> 3 (bits ``[18:3]``) + - Coarsest bucket; also the ``qsel`` default/fallback case. + +This is a coarse, power-of-two headroom scheme (not an exact +divide-by-tone-count normalization) -- e.g. 3 active tones are divided by 4, +not 3, and anywhere from 5 to 8 active tones are all divided by 8. + +3.3 Streaming Control -- Mask and Duration (``ctrl.sv``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Frequency/phase/gain are static, slow-path AXI-Lite settings. What the +tProcessor actually streams in per pulse, over ``s_axis`` (40 bits wide, +queued in a 16-entry FIFO), is just: + +.. list-table:: S_AXIS descriptor word (40 bits, ``ctrl.sv``) + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Bits + - Description + * - ``mask`` + - ``[39:32]`` (8 bits) + - One bit per tone (bit *i* enables tone *i*). Tones not in the mask + are muted for this burst (see 3.2). + * - ``nsamp`` + - ``[31:0]`` (32 bits) + - Number of output samples (cycles) to hold this mask before the FSM + looks for the next queued descriptor. + +``ctrl.sv``'s FSM (``READ_ST -> CNT0_ST -> CNT_ST -> READ_ST``) dequeues one +descriptor, holds the output enabled (``m_axis_tvalid`` asserted, mask +applied) for ``nsamp`` cycles, then immediately starts the next queued +descriptor if the FIFO is non-empty -- so back-to-back mask/length +descriptors play with no gap between them, and the output only drops +(``en``/``m_axis_tvalid`` deasserted) once the FIFO runs dry. Several small +``latency_reg`` shift-register instances (in ``ctrl.sv``, ``dds_top.v`` and +``sg_mux8.v``) exist purely to align the ``mask``/enable control signals +with the multi-stage DDS -> multiply -> summer pipeline; this document does +not attempt to enumerate the combined end-to-end pipeline latency, since +that was not directly measured/simulated in this pass. + +In a full tProc-v2 design (verified against +``firmware/projects/qick_tprocv2_216_demo/bd_2023-1.tcl``), the tProcessor +does not drive this 40-bit format directly -- an ``sg_translator`` IP sits +between the tProc's common per-generator wave descriptor and this core's +``s_axis``, repacking it into the ``{mask, nsamp}`` format above. + +-------------------------------------------------------------------- +4. Register Map +-------------------------------------------------------------------- + +All registers are accessed through a standard 32-bit-word AXI-Lite slave +(``axi_slv.v``, 8-bit address bus, 64 x 32-bit word register file -- only +the first 25 words are used by this core). Byte offset = word index x 4. + +.. list-table:: + :header-rows: 1 + :widths: 22 12 14 52 + + * - Register + - Byte offset + - Width + - Description + * - ``PINC0_REG`` .. ``PINC7_REG`` + - 0x00 .. 0x1C + - 32 bits + - Per-tone DDS frequency tuning word (one register per tone, word + index = tone number). + * - ``POFF0_REG`` .. ``POFF7_REG`` + - 0x20 .. 0x3C + - 32 bits + - Per-tone phase offset. + * - ``GAIN0_REG`` .. ``GAIN7_REG`` + - 0x40 .. 0x5C + - 32-bit AXI-Lite slot; **only bits [15:0] reach the datapath** + - Per-tone signed gain. The AXI-Lite register itself is a full 32-bit + word (like every other register in this block's ``axi_slv``), but + the ``GAINx_REG`` ports on ``sg_mux8.v``/``dds_top.v`` are declared + only 16 bits wide, so the upper 16 bits written over AXI-Lite are + silently dropped before they reach the multiplier. This was + confirmed by reading the port declarations in ``sg_mux8.v`` and + ``dds_top.v`` (16-bit) against ``axi_slv.v`` (32-bit output ports). + * - ``WE_REG`` + - 0x60 + - 1 bit (bit 0 of a 32-bit word) + - Write-enable pulse. Toggling 0->1->0 latches the currently-written + ``PINCx``/``POFFx`` values into the phase-coherent controllers of + all 8 tones simultaneously (``GAINx`` is not gated by this bit -- + see 3.1). + +-------------------------------------------------------------------- +5. Python Interface +-------------------------------------------------------------------- + +The driver is ``qick.drivers.generator.AxisSgMux8V1`` +(``bindto = ['user.org:user:axis_sg_mux8_v1:1.0', 'QICK:QICK:axis_sg_mux8_v1:1.0']``), +a thin subclass of ``AbsMuxSignalGen`` with: + +.. code-block:: python + + HAS_MIXER = False + B_DDS = 32 + N_TONES = 8 + HAS_GAIN = True + HAS_PHASE = True + B_PHASE = 32 + +``AbsMuxSignalGen._init_config()`` builds the register-name-to-word-index +map programmatically from these flags -- ``pinc{i}_reg`` at words 0-7, +``poff{i}_reg`` at words 8-15 (since ``HAS_PHASE``), ``gain{i}_reg`` at +words 16-23 (since ``HAS_GAIN``), then ``we_reg`` at word 24 -- which +matches the RTL register map in section 4 exactly (verified by reading both +sides). + +**Normal usage** goes through :meth:`.QickProgram.declare_gen` and the +per-pulse ``mask``/``length`` parameters, not direct register access: + +.. code-block:: python + :caption: Configuring and playing tones on a mux8 generator + + from qick import * + + soc = QickSoc() + # ch = the index of the axis_sg_mux8_v1 channel in soc['gens'] + + class MyProgram(QickProgram): + def initialize(self): + # Declare up to 8 tones; unlisted mask indices play silence. + self.declare_gen(ch=ch, nqz=2, + mux_freqs=[100.0, 150.0, 220.5], # MHz + mux_gains=[1.0, 0.8, 0.5], # -1..1 + mux_phases=[0, 0, 90]) # degrees + + def body(self): + # Play tones 0 and 2 together for 1000 samples. + self.pulse(ch=ch, style='const', mask=[0, 2], length=1000) + +``declare_gen`` calls :meth:`.QickProgram.calc_muxgen_regs` to convert the +float MHz/gain/degrees values into raw ``freq_int``/``gain_int``/ +``phase_int`` register values (via :meth:`.QickProgram.freq2reg` / +:meth:`.QickProgram.deg2reg`, using ``B_DDS = 32``/``B_PHASE = 32``, and +``gain_int = round(gain * MAXV)`` with ``MAXV = 2**15-2``), which +``config_gens()`` then pushes into the generator via +``AxisSgMux8V1.set_tones_int()`` (which loops over the tones, writes +``pincX_REG``/``gainX_REG``/``poffX_REG`` for each configured tone and +zeroes the gain of any unconfigured tone up to ``N_TONES``, then pulses +``we_reg``). The pulse's ``mask``/``length`` parameters are packed by +``MultiplexedGenManager`` (``qick.asm_v1``/``qick.asm_v2``) into the +40-bit ``{mask, nsamp}`` descriptor described in section 3.3. + +.. note:: + + ``AbsMuxSignalGen.set_tones()`` (a debug-only convenience method, not + normally called by user code) calls ``self.set_all_int(tones)`` -- + but ``set_all_int`` is not defined anywhere on ``AbsMuxSignalGen`` or + its base classes (only ``set_tones_int`` is). This looks like a + leftover/typo in the current driver source (``qick_lib/qick/drivers/generator.py``) + and would raise an ``AttributeError`` if called; use + ``set_tones_int()`` directly, or the normal ``declare_gen()`` path + above, instead. + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`sg_v6` -- the single-tone arbitrary-envelope generator, for + contrast with this fixed-tone mux design. +* :doc:`/tprocv2_trm` -- tProcessor v2 instruction reference, including the + wave-descriptor mechanism translated by ``sg_translator`` into this core's + mask/length format. +* :doc:`../index` -- firmware overview and channel assignments. +* `axis_sg_mux8_v1 source code `_ diff --git a/docs/source/firmware/generators/sg_v4.rst b/docs/source/firmware/generators/sg_v4.rst new file mode 100644 index 000000000..e4f2ea77a --- /dev/null +++ b/docs/source/firmware/generators/sg_v4.rst @@ -0,0 +1,471 @@ +======================================================== +Signal Generator v4 (SG-v4) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_signal_gen_v4** is QICK's original tProc-controlled arbitrary-waveform +generator: a single real-valued output lane fed by ``N_DDS`` parallel +DDS-plus-mixer channels, each pairing one complex multiplier with its own +envelope BRAM. It is the direct predecessor of :doc:`sg_v6`, and the two +share the same Python driver class (``qick.drivers.generator.AxisSignalGen``) +-- its docstring states plainly that the class "Supports AxisSignalGen +V4+V5+V6, since they have the same software interface (ignoring registers +that are not used)". The module lives in the ``qick`` firmware repository +under ``firmware/ip/axis_signal_gen_v4/``. + +Unlike SG-v6, SG-v4 does not appear in any currently-shipped board project +in this repository (no ``firmware/Projects/**`` block-design script +instantiates ``axis_signal_gen_v4``); it is documented here as the legacy +core the Python driver still supports, and to make the concrete differences +from SG-v6 explicit for anyone reading old designs or the driver's +backward-compatibility code paths. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +The core is built from four RTL blocks (``axis_signal_gen_v4.v`` instantiates +the first two; ``signal_gen_top.v`` instantiates the rest): + +* ``axi_slv`` (``axi_slv.vhd``) -- the AXI4-Lite configuration register file. +* ``signal_gen_top`` (``signal_gen_top.v``) -- wires up the waveform FIFO, + the envelope-loading state machine, the per-lane BRAMs, and the + ``signal_gen`` core. +* ``data_writer`` (``data_writer.vhd``) -- an FSM that streams envelope + samples from ``s0_axis`` into the interleaved per-lane BRAMs. +* ``signal_gen`` (``signal_gen.v``, with ``ctrl.sv`` as its descriptor-decode + FSM) -- reads queued waveform descriptors, drives ``N_DDS`` parallel + ``dds_compiler_0`` instances, multiplies each against its envelope sample, + and muxes/rounds the result onto ``m_axis``. + +``AxisSignalGen.HAS_MIXER = False`` in the Python driver: there is no +DAC-side digital mixer downstream of this block (unlike the interpolated +``axis_sg_int4_v*`` generators) -- all upconversion is done by the per-lane +DDS itself, which is why :meth:`.QickProgram.declare_gen` never asks for a +``mixer_freq`` on this channel. + +Two source files in ``firmware/ip/axis_signal_gen_v4/src/`` -- ``dither.v`` +and ``random_gen.v`` -- are **not part of the active datapath**; see +:ref:`sgv4-dithering` below. + +.. figure:: /images/firmware/sg_v4-blocks.svg + :align: center + :width: 85% + + Top-level block diagram. Same envelope-memory/FIFO building blocks as + SG-v6, but the envelope x DDS complex product goes through a single + ``Gain`` stage and then a *static* real/imag select (``OUTSEL_REG``, + set once from Zynq) instead of SG-v6's per-descriptor 3-way mux -- + this core always runs the DDS path and outputs one 16-bit value per + lane. + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``N`` + - 12 + - Envelope memory address width. Each of the ``N_DDS`` lanes owns a + real-part BRAM and an imaginary-part BRAM, each ``2**N`` samples + deep x 16 bits wide. With the default, that's 4096 samples/lane, + 65536 samples total across the interleaved lanes -- this is what the + Python driver reports as ``cfg['maxlen']`` (``2**N * N_DDS``, in + ``AxisSignalGen._init_config``). SG-v6's top-level default for the + same parameter is ``N = 16`` (see :doc:`sg_v6`); the value actually + burned into a given bitstream depends on how the block design + instantiated the core, so read ``soc.gens[i]['maxlen']`` at runtime + rather than assuming either default. + * - ``N_DDS`` + - 16 + - Number of parallel DDS + complex-multiplier lanes, and therefore + samples emitted per ``aclk`` cycle on ``m_axis_tdata`` (bus width + ``N_DDS * 16`` bits). Exposed to Python as ``SAMPS_PER_CLK = 16`` on + the driver class. + +SG-v4 has no ``GEN_DDS`` or ``ENVELOPE_TYPE`` synthesis parameters -- +``axis_signal_gen_v4.v`` declares only ``N`` and ``N_DDS``. Both DDS lanes +and imaginary-part BRAMs are therefore always present; there is no +baseband-only or real-only-envelope build option (both were added in +SG-v6 -- see :ref:`sgv4-vs-sgv6`). Correspondingly, +``AbsPulsedSignalGen._init_config`` falls back to the class default +``HAS_DDS = True`` and ``AbsArbSignalGen._init_config`` falls back to +``COMPLEX_ENVELOPE = True`` for this core, since neither ``GEN_DDS`` nor +``ENVELOPE_TYPE`` is present in its parameter dict. + +-------------------------------------------------------------------- +3. Datapath +-------------------------------------------------------------------- + +3.1 Waveform descriptor queue (``s1_axis``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``s1_axis_tdata`` is a 160-bit waveform descriptor, pushed straight into a +16-entry, 160-bit-wide FIFO (``fifo.vhd``, ``.B(160)``, ``.N(16)``) with +``fifo_wr_en = s1_axis_tvalid`` and ``s1_axis_tready = ~fifo_full`` -- there +is no register staging on the write side, unlike ``axis_readout_v2``'s +``WE_REG``-pulsed descriptor push (:doc:`../readouts/readout_v2`). ``ctrl.sv`` decodes +the descriptor at the head of the FIFO: + +.. list-table:: Waveform descriptor fields (``ctrl.sv`` header comment) + :header-rows: 1 + :widths: 15 15 15 55 + + * - Field + - Bits + - Width + - Description + * - ``freq`` + - ``[31:0]`` + - 32 + - DDS frequency tuning word for this pulse. + * - ``phase`` + - ``[63:32]`` + - 32 + - DDS phase offset for this pulse. + * - ``addr`` + - ``[79:64]`` + - 16 + - Envelope-memory start address to read for this pulse (**not** the + same register as ``START_ADDR_REG`` below -- that one is only used + when bulk-loading samples over ``s0_axis``). + * - ``gain`` + - ``[111:96]`` + - 16 + - Signed output gain. + * - ``nsamp`` + - ``[143:128]`` + - 16 + - Number of ``aclk`` cycles (each emitting ``N_DDS`` samples) to stay + in the active ``CNT_ST`` state before returning to ``READ_ST``. + * - ``outsel`` + - ``[145:144]`` + - 2 + - Per-pulse output-source select: 0 = product (DDS x envelope), 1 = + DDS only, 2 = envelope only, 3 = zero. **Not the same control as the + ``OUTSEL_REG`` AXI register** described in :ref:`sgv4-regmap` -- + despite the similar name, that register picks the real/imaginary + component of whatever this field selects, chip-wide, not per pulse. + * - ``mode`` + - ``146`` + - 1 + - 0: play for ``nsamp`` cycles and stop. 1: play continuously + (periodic). + * - ``stdysel`` + - ``147`` + - 1 + - Steady-value select; see :ref:`sgv4-steady` below. + * - ``phrst`` + - ``148`` + - 1 + - Documented in the header comment table but **not read anywhere in + ``ctrl.sv``'s body** -- ``fifo_dout_r[148]`` is never referenced. + Setting this bit has no effect in SG-v4. SG-v6's ``ctrl_sg_v6.sv`` + *does* wire the equivalent bit (``phrst_int = fifo_dout_r[148]``) + into a synchronous phase-accumulator reset -- see + :ref:`sgv4-vs-sgv6`. + +The FSM (``READ_ST`` / ``CNT_ST``) reads a new descriptor whenever it's idle +and the FIFO isn't empty (or ``mode`` is periodic), then holds in +``CNT_ST`` for ``nsamp`` cycles. ``gain``, the 2-bit ``src`` (outsel), +``stdysel``, and the enable flag are each pushed through long shift-register +pipelines (4 stages inside ``ctrl.sv``, then a further 13-19 stages inside +``signal_gen.v``) to stay aligned with the DDS and multiplier latency before +they reach the output mux. + +3.2 Envelope loading (``s0_axis``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``data_writer`` (``data_writer.vhd``) is a small FSM +(``INIT -> READ_START_ADDR -> WAIT_TVALID -> RW_TDATA``) that writes +32-bit samples arriving on ``s0_axis_tdata`` (bits ``[15:0]`` = real, +``[31:16]`` = imaginary) into the per-lane BRAM pair. It keeps one flat +running address (``mem_addr_full``); the low ``ceil(log2(N_DDS))`` = 4 bits +select which of the ``N_DDS`` lanes' ``mem_ena`` to assert, and the +remaining ``N`` bits become that lane's BRAM address -- i.e. samples are +written round-robin across the 16 lanes. ``WE_REG`` is resynchronized into +the ``s0_axis_aclk`` domain with a 2-stage ``synchronizer_n`` before it can +move the FSM out of ``INIT``, and the counter is reloaded from +``START_ADDR_REG`` each time a new write burst starts. + +Each lane's real and imaginary BRAMs are separate ``bram_dp`` instances +(the shared dual-port BRAM primitive under ``firmware/hdl/``, not the +single-port ``bram.v`` file that ships in this IP's own ``src/`` -- that +file is unused, see :ref:`sgv4-dithering`). Write port A runs on +``s0_axis_aclk``; read port B runs on ``aclk`` and is driven by +``signal_gen``. + +3.3 Per-lane DDS and complex multiply +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Each of the ``N_DDS`` lanes instantiates its own ``dds_compiler_0`` core, +phase-offset from its neighbors by ``pinc * i`` (lane ``i`` represents time +step ``i`` within the current ``N_DDS``-wide parallel block, so the 16 +lanes together form one coherent, phase-continuous tone). ``signal_gen.v`` +then computes the **full complex product** of the DDS output (I, Q) against +the envelope sample (I, Q) read back from BRAM: + +.. code-block:: text + + prod_real = dds_I * env_I - dds_Q * env_Q + prod_imag = dds_I * env_Q + dds_Q * env_I + +That's four 16x16 multiplies per lane, even though (per +:ref:`sgv4-outsel-note` below) only one of ``prod_real``/``prod_imag`` is +ever driven onto the output. SG-v6 removed the two multiplies needed for +``prod_imag`` entirely, since it never produces a usable imaginary output +(see :ref:`sgv4-vs-sgv6`). + +.. _sgv4-outsel-note: + +3.4 Output source mux, real/imag select, and rounding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``dout_mux[i]`` picks one 32-bit ``{imag, real}`` word per the per-pulse +``outsel`` descriptor field (product / DDS-only / envelope-only / zero, +see 3.1), and that word is scaled by ``gain`` (one more signed 16x16 +multiply) and rounded (bits ``[30:15]`` of the 32-bit gained product, i.e. +truncation after the gain multiply, no separate rounding-add term). The +final 16-bit sample driven onto ``m_axis_tdata[i*16 +: 16]`` picks between +the rounded real and imaginary halves using **``OUTSEL_REG``**, a +chip-wide AXI-Lite register (0 = real, 1 = imaginary) -- *not* the +per-pulse ``outsel`` descriptor field. Because ``m_axis_tdata`` only +carries one 16-bit sample per lane per cycle (not a 32-bit I/Q pair), a +single generator instance can only ever emit the real *or* the imaginary +component of its internal complex datapath, chosen once for the whole +core via this register, not per pulse. + +.. _sgv4-steady: + +3.5 Enable / steady-value gating +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``m_axis_tdata[i] = (~en & stdysel) ? 0 : round_r_mux[i]``. When the +generator is disabled (between pulses, or after a one-shot ``mode=0`` +pulse finishes) and ``stdysel`` is set, the output is forced to zero; +otherwise the (possibly stale, still-pipelined) ``round_r_mux`` value +passes through unchanged. SG-v4 has no dedicated "hold the last sample" +state -- see :ref:`sgv4-vs-sgv6` for how SG-v6 differs here. + +.. _sgv4-dithering: + +3.6 Dithering RTL is present but disconnected +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``firmware/ip/axis_signal_gen_v4/src/`` contains ``dither.v`` (adds +LFSR-generated pseudo-random noise, scaled by a register-controlled shift +amount, ahead of a bit-truncation -- a standard dithering-before-requantize +technique) and ``random_gen.v`` (a parameterizable-width Fibonacci-tap +LFSR). Neither module is instantiated anywhere in +``signal_gen_top.v``/``signal_gen.v``; the only place ``dither`` is +referenced is inside its own file, instantiating ``random_gen``. The +``RNDQ_REG`` register (see :ref:`sgv4-regmap`) is wired all the way from +``axi_slv`` down through ``signal_gen_top`` into ``signal_gen.v``'s port +list, but ``signal_gen.v``'s body never reads it. This is corroborated on +the Python side: ``AxisSignalGen.rndq()`` is annotated +``"TODO: remove this function. This functionality was removed from IP +block."``, and the register is set once to a placeholder value (``10``) in +``_init_firmware`` and never touched again. Treat SG-v4's dithering support +as vestigial source code, not a working feature. (A separate, +independently-maintained register-description package in this repo, +``firmware/fusesoc/cores/ip/axis_signal_gen_v4/rdl/``, describes +``RNDQ_REG`` the same way: "Deprecated (rounding-select removed). Retained +as a dummy for register-map / driver bindto compatibility (no functional +effect)" -- consistent with what the RTL shows, though that package +describes a different, non-synthesized register model and should not be +read as documenting this IP's actual register file.) + +.. _sgv4-regmap: + +-------------------------------------------------------------------- +4. Register Map +-------------------------------------------------------------------- + +``axi_slv.vhd`` implements a generic 16-register, 6-bit-address AXI4-Lite +block (word-addressed offsets ``0x00``-``0x3C``), but only wires the first +four registers to anything; the remaining twelve are plain, unconnected +flip-flops (writable and readable, but with no effect on the datapath). + +.. list-table:: + :header-rows: 1 + :widths: 22 12 15 51 + + * - Register + - Offset + - Width + - Description + * - ``START_ADDR_REG`` + - ``0x00`` + - 32 bits + - Flat start address (lane-interleaved) for the next bulk envelope + write over ``s0_axis``. Loaded into ``data_writer``'s address + counter when ``WE_REG`` rises. + * - ``WE_REG`` + - ``0x04`` + - 1 bit (``slv_reg1[0]``) + - Envelope-memory write enable. Level-sensitive, resynchronized into + the ``s0_axis_aclk`` domain. + * - ``RNDQ_REG`` + - ``0x08`` + - 32 bits + - Comment in ``axi_slv.vhd``: "Noise amplitude for dithering." Not + actually connected to any dithering logic -- see + :ref:`sgv4-dithering`. + * - ``OUTSEL_REG`` + - ``0x0C`` + - 1 bit (``slv_reg3[0]``) + - Chip-wide real/imaginary output select (0 = real, 1 = imaginary); + see :ref:`sgv4-outsel-note`. **Not present in the Python driver's + ``REGISTERS`` dict** (``AxisSignalGen.REGISTERS = {'start_addr_reg': + 0, 'we_reg': 1, 'rndq_reg': 2}``), so it can't be written from + Python -- it stays at its post-reset value, which is ``0`` (real + component) since all ``slv_reg`` are cleared to zero on + ``aresetn``. + +All registers are simple ``std_logic_vector`` flops written a full 32 bits +at a time (byte-strobed) on the ``s_axi_aclk`` domain; there is no +write-pulse/handshake semantics beyond the level of ``WE_REG`` itself. + +.. _sgv4-vs-sgv6: + +-------------------------------------------------------------------- +5. Differences from SG-v6 +-------------------------------------------------------------------- + +The two cores share a Python driver and a wire-compatible 160-bit +descriptor format, but the RTL differs in several concrete ways: + +.. list-table:: + :header-rows: 1 + :widths: 30 35 35 + + * - Aspect + - SG-v4 + - SG-v6 + * - Complex product + - Computes both real and imaginary parts of DDS x envelope (4 + multiplies/lane); only one is ever output. + - Computes only the real part (2 multiplies/lane); the imaginary + partial products are commented out of ``signal_gen.v`` entirely. + * - Real/imag output select + - Chip-wide ``OUTSEL_REG`` AXI register (not exposed by the Python + driver). + - Removed -- ``axi_slv_sg_v6.vhd`` only implements + ``START_ADDR_REG``/``WE_REG``. + * - Baseband-only mode + - Not available -- DDS lanes are always instantiated. + - ``GEN_DDS = "FALSE"`` synthesis parameter removes the DDS + compilers, for baseband-envelope-only builds. + * - Real-only envelope + - Not available -- both real and imaginary BRAMs are always + instantiated per lane. + - ``ENVELOPE_TYPE = "REAL"`` synthesis parameter drops the imaginary + BRAM and forces ``mem_dob_imag`` to zero. + * - ``phrst`` descriptor bit + - Defined in the descriptor format comment (bit 148) but never read + in ``ctrl.sv``. No effect. + - Read (``phrst_int = fifo_dout_r[148]``) and used to reset the phase + accumulator synchronously with a new descriptor load. + * - Dithering + - ``dither.v``/``random_gen.v`` present in ``src/`` but not + instantiated; ``RNDQ_REG`` exists but is unconnected. Vestigial. + - Files and register removed entirely. + * - Idle/steady output + - Forces output to zero when disabled and ``stdysel`` is set; + otherwise the last pipelined value passes through as-is (no + explicit hold). + - Adds an explicit ``last_r`` register that latches the final sample + whenever enabled, so the idle output can hold that value + (``stdysel=0``) or force zero (``stdysel=1``). + * - Register pipelining style + - Manually chained ``reg`` stages (``_r1``, ``_r2``, ... up to + ``_r19`` in places). + - A reusable parameterized ``latency_reg`` module for each delay + line. + * - Top-level ``N`` default + - 12 (4096 samples/lane). + - 16 (65536 samples/lane). + +From a QICK-program-writer's point of view these are resource/feature +differences, not API differences: both bind to the same +``qick.drivers.generator.AxisSignalGen`` class +(``bindto = ['user.org:user:axis_signal_gen_v4:1.0', ... +'axis_signal_gen_v6:1.0', ...]``), so :meth:`.QickProgram.declare_gen`, +:meth:`.QickProgram.add_envelope`, and pulse-playback instructions work +identically on either version -- you simply lose the ability to select the +imaginary output, use per-pulse phase reset, or run in baseband/real-only +modes on SG-v4. + +-------------------------------------------------------------------- +6. Python Interface +-------------------------------------------------------------------- + +SG-v4 is reached through the same class hierarchy as every other QICK +pulsed generator (``qick.drivers.generator``): + +* ``AbsSignalGen`` -- ``MAXV = 2**15-2``; Nyquist-zone and (if + ``HAS_MIXER``) mixer-frequency helpers. +* ``AbsArbSignalGen(AbsSignalGen)`` -- adds the envelope DMA path + (``load()``, over ``s0_axis``) and ``_wr_enable``/``_wr_disable`` + (``START_ADDR_REG``/``WE_REG``). +* ``AbsPulsedSignalGen(AbsSignalGen)`` -- adds the tProc-facing ``s1_axis`` + port and DDS/phase width config (``B_DDS``, ``B_PHASE``, ``HAS_DDS``). +* ``AxisSignalGen(AbsArbSignalGen, AbsPulsedSignalGen)`` -- the concrete + class, with ``bindto`` covering the v4/v5/v6 IP-core VLNV strings, + ``HAS_MIXER = False``, ``SAMPS_PER_CLK = 16``, ``B_DDS = B_PHASE = 32``. + +Because ``ENVELOPE_TYPE``/``GEN_DDS`` aren't present in SG-v4's synthesis +parameters, ``cfg['complex_env']`` and ``cfg['has_dds']`` fall back to the +class defaults (``True``/``True``) for this core. + +Most QICK programs never touch this driver directly -- pulses are declared +through :meth:`.QickProgram.declare_gen` and +:meth:`.QickProgram.add_envelope`/:meth:`.QickProgram.add_gauss`, and +played via the normal tProc pulse instructions, exactly as for SG-v6. The +one thing worth calling out for SG-v4 specifically is that ``OUTSEL_REG`` +is not reachable from Python (see :ref:`sgv4-regmap`): if you need the +imaginary component of the DDS x envelope product out of an SG-v4 +channel, there is no driver method for it -- you would have to write the +register through the low-level MMIO interface yourself. + +.. code-block:: python + :caption: Loading an envelope and playing an SG-v4 pulse + + from qick import * + import numpy as np + + soc = QickSoc() + gen_ch = 0 # an SG-v4 channel on this board + + soc.declare_gen(ch=gen_ch, nqz=1) # no mixer_freq: HAS_MIXER is False + + # Gaussian envelope, real part only (imaginary defaults to zero if omitted) + length = 100 + sigma = length / 5 + idata = 30000 * np.exp(-0.5 * ((np.arange(length) - length/2) / sigma)**2) + soc.add_envelope(ch=gen_ch, name="gauss", idata=idata.astype(np.int16)) + + soc.set_pulse_registers(ch=gen_ch, style="arb", envelope="gauss", + freq=soc.freq2reg(100.0, gen_ch=gen_ch), + phase=0, gain=30000, length=length) + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`sg_v6` -- the successor core; see :ref:`sgv4-vs-sgv6` above for + the concrete RTL differences. +* :doc:`/tprocv2_trm` -- tProcessor sequencing/triggering that queues + waveform descriptors into this core's ``s1_axis``. +* :doc:`../index` -- firmware overview and channel assignments. +* :doc:`../readouts/readout_v2` -- a similarly-legacy, single-tone predecessor core on + the readout (ADC) side, for comparison. diff --git a/docs/source/firmware/generators/sg_v6.rst b/docs/source/firmware/generators/sg_v6.rst new file mode 100644 index 000000000..a48492ee4 --- /dev/null +++ b/docs/source/firmware/generators/sg_v6.rst @@ -0,0 +1,515 @@ +======================================================== +Signal Generator v6 (SG-v6) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +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_gen`` block 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. + +.. figure:: /images/firmware/sg_v6-blocks.svg + :align: center + :width: 80% + + 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 +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - Default Value + - Description + * - ``N`` + - 16 + - Memory address bus size (number of bits). The effective table depth + is **2^N** samples per bank. + * - ``N_DDS`` + - 16 + - Number of parallel DDS blocks and, therefore, samples emitted per clock + cycle on the ``m_axis_tdata_o`` output bus (total width = ``N_DDS × 16`` bits). + * - ``GEN_DDS`` + - ``"TRUE"`` + - ``"TRUE"`` instantiates DDS blocks for upconversion. ``"FALSE"`` generates only + the baseband envelope. + * - ``ENVELOPE_TYPE`` + - ``"COMPLEX"`` + - ``"COMPLEX"`` instantiates separate BRAMs for real and imaginary parts. ``"REAL"`` + uses only the real BRAM and assigns zero to the imaginary part. + +-------------------------------------------------------------------- +3. Interface Ports +-------------------------------------------------------------------- + +3.1 Clock and Reset +^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 10 50 + + * - Signal + - Direction + - Description + * - ``aresetn`` + - input + - Active-low reset for the main clock domain (``aclk``). + * - ``aclk`` + - input + - Main clock. Drives the FIFO, the ``signal_gen`` block, and the output bus. + +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``. + +.. list-table:: + :header-rows: 1 + :widths: 30 10 10 50 + + * - Signal + - Direction + - Width + - Description + * - ``s0_axis_aresetn`` + - input + - 1 + - Active-low reset for the S0 domain. + * - ``s0_axis_aclk`` + - input + - 1 + - S0 domain clock. Can differ from ``aclk`` (dual-clock BRAM). + * - ``s0_axis_tdata_i`` + - input + - 32 + - Sample data. Bits [15:0] = real part; bits [31:16] = imaginary part + (only written if ``ENVELOPE_TYPE = "COMPLEX"``). + * - ``s0_axis_tvalid_i`` + - input + - 1 + - Indicates valid data on the bus (standard AXI-Stream protocol). + * - ``s0_axis_tready_o`` + - 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. + +.. list-table:: + :header-rows: 1 + :widths: 30 10 10 50 + + * - Signal + - Direction + - Width + - Description + * - ``s1_axis_tdata_i`` + - input + - 160 + - 160-bit waveform descriptor. The ``signal_gen`` block interprets its + content internally. + * - ``s1_axis_tvalid_i`` + - input + - 1 + - Indicates a valid descriptor. Connected directly to ``fifo_wr_en``. + * - ``s1_axis_tready_o`` + - output + - 1 + - Asserted when the internal FIFO is not full (``~fifo_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. + +.. list-table:: + :header-rows: 1 + :widths: 30 10 10 50 + + * - Signal + - Direction + - Width + - Description + * - ``m_axis_tready_i`` + - input + - 1 + - Consumer indicates it can accept data. + * - ``m_axis_tvalid_o`` + - output + - 1 + - Module indicates that ``m_axis_tdata_o`` contains valid data. + * - ``m_axis_tdata_o`` + - output + - ``N_DDS × 16`` + - Output sample bus. With default values (``N_DDS = 16``), the width is + **256 bits**, corresponding to 16 samples of 16 bits per cycle. + +3.5 Control Registers +^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + :widths: 30 10 10 50 + + * - Signal + - Direction + - Width + - Description + * - ``START_ADDR_REG`` + - input + - 32 + - Starting address in BRAM where the ``data_writer`` will begin writing + samples received via S0_AXIS. + * - ``WE_REG`` + - input + - 1 + - BRAM write enable. When asserted, the ``data_writer`` propagates the + write to the selected BRAM. + +-------------------------------------------------------------------- +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_full`` flag controls the ``s1_axis_tready_o`` signal (backpressure). +* The ``signal_gen`` block 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_aclk`` domain. Receives data from the ``data_writer``. +* **Port B (Read):** ``aclk`` domain. The ``signal_gen`` provides the address + ``mem_addrb`` and 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 :class:`.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 +:doc:`../index`'s :ref:`tproc-zcu216-example`). 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``. + +.. code-block:: python + :caption: Basic SG-v6 pulse from Python + + 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 +:meth:`.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. + +.. code-block:: python + :caption: Shaped pulse with a Gaussian envelope + + 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 :meth:`.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. + +.. code-block:: python + :caption: Queueing several pulses on the same channel + + 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``. + +.. list-table:: SG-v6 Descriptor Fields (r_wave composition) + :header-rows: 1 + :widths: 15 20 15 50 + + * - Field + - Register + - Bits + - Description + * - Frequency + - ``w_freq`` + - 32 + - DDS frequency control word (0 = DC, 2^31 = Nyquist) + * - Phase + - ``w_phase`` + - 32 + - DDS phase offset (2^32 = 360°) + * - Gain + - ``w_gain`` + - 32 + - Amplitude scaling (signed 32-bit, max 2^31-1) + * - Envelope Start + - ``w_env`` + - 24 + - Start address in envelope memory (for shaped pulses) + * - Length + - ``w_length`` + - 32 + - Number of samples to output (for shaped pulses) + * - Configuration + - ``w_conf`` + - 16 + - Mode, output selection, etc. (see SG-v6 source) + +.. note:: + + The 160-bit descriptor sent via S1_AXIS is packed internally by + :meth:`.Waveform.compile` (``qick.asm_v2``) from the fields you pass to + :meth:`.QickProgram.add_pulse`/:meth:`.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**: :doc:`/tutorials/06_Generators_And_Readouts`, + :doc:`/tutorials/07_Advanced_Generators_And_Readouts` +- **tProcessor Documentation**: :doc:`/tprocv2_trm` +- **Firmware Overview**: :doc:`../index` +- **Community Examples**: Check the `#qick` channel on the Unitary Fund Discord + +-------------------------------------------------------------------- +7. Implementation Considerations +-------------------------------------------------------------------- + +**Clock Domains:** + The module contains two clock domains (``s0_axis_aclk`` and ``aclk``). Domain + crossing is resolved via dual-clock BRAMs. Ensure appropriate timing constraints + are applied in the XDC file. + +**BRAM Read Latency:** + The ``bram_dp_xpm`` instances have ``OUT_REG_ENA = 1``, adding one cycle + of latency to the read port to improve timing. + +**Output Bus Width:** + The ``m_axis_tdata_o`` bus is ``N_DDS × 16`` bits wide. With the default + ``N_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_o`` + signal must be managed. + +-------------------------------------------------------------------- +8. Default Parameters – Quick Summary +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Configuration + - Value + * - Samples per cycle (M_AXIS) + - 16 (``N_DDS = 16``) + * - Output bus width + - 256 bits + * - Table depth per bank + - 65,536 samples (``N = 16``) + * - 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). + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`index` - overview of all QICK signal generator types and when to use each +* :doc:`/tprocv2_trm` - tProcessor v2 for sequencing and triggering +* :doc:`../index` - Firmware overview and channel assignments +* :doc:`sg_mux8`, :doc:`sg_mixmux8` - multi-tone generators, for playing several simultaneous fixed tones instead of one arbitrary-envelope pulse +* :doc:`sg_int4_v2` - a lighter single-tone arbitrary generator (see :doc:`index` for the tradeoffs) +* :doc:`sg_v4` - the legacy predecessor to SG-v6 +* `SG-v6 source code `_ diff --git a/docs/source/firmware/index.rst b/docs/source/firmware/index.rst new file mode 100644 index 000000000..c1263e680 --- /dev/null +++ b/docs/source/firmware/index.rst @@ -0,0 +1,546 @@ +Firmware +======== + +.. toctree:: + :maxdepth: 2 + :caption: 5. Firmware + :hidden: + + generators/index + readouts/index + support_cores + +This page provides an overview of the QICK firmware components. + +System Overview +--------------- + +The QICK firmware includes the following components: + +* **tProcessor v2** - Real-time co-processor for sequencing and feedback +* **Signal Generators** - DDS-based waveform generators for DAC outputs; QICK + ships several generator cores (:doc:`generators/sg_v6` being the flagship + arbitrary-envelope one), and a board's standard firmware provisions its own + mix -- see :doc:`generators/index` +* **Readout System** - ADC data acquisition with DDC, filtering, and averaging + +.. list-table:: + :header-rows: 1 + + * - Component + - Quantity + - Documentation + * - tProcessor v2 + - 1 instance (64-bit instructions, 32-bit registers) + - :doc:`/tprocv2_trm` + * - Signal generators + - Board-dependent -- each board's standard firmware provisions its own + mix of :doc:`generators/sg_v6`, :doc:`generators/sg_int4_v2`, :doc:`generators/sg_mixmux8`, + :doc:`generators/sg_mux8`; see :ref:`std-firmware-per-board` + - :doc:`generators/index`, :doc:`generators/sg_v6` + * - Readouts + - Board-dependent -- single-tone (:doc:`readouts/readout_v2`, + :doc:`readouts/readout_dynamic`) and/or channelized (:doc:`readouts/readout_pfb`); + see :ref:`std-firmware-per-board` + - :doc:`readouts/index`, :doc:`readouts/readout_dynamic`, :doc:`readouts/readout_pfb`, + :doc:`readouts/avg_buffer` + * - tProc trigger outputs (``trig_N_o`` pins) + - Board-dependent single-bit pins -- PMOD bits plus one per averaging + buffer, plus DDR4/multi-rate-buffer triggers where present; see + :ref:`tproc-ports` + - :doc:`/tprocv2_trm` + +Sampling frequency of ADC blocks is given by the variable ``soc.fs_adc``. +Sampling frequency of DACs is stored in variable ``soc.fs_dac``. +Fast-speed buffers were removed to save memory space. Raw data can be captured after x8 down-sampling. + +.. figure:: /images/firmware/firmware-overview.svg + :align: center + :width: 85% + + System-level block diagram: the tProcessor sequences the signal + generators (SG) and readouts, which sit between it and the DACs/ADCs; + the Zynq PS configures everything over AXI (purple) and is the boundary + between the PS (software) and PL (FPGA fabric) sides of the chip. + +Signal Generators +----------------- + +QICK ships several signal-generator IP cores for the DAC-output stage -- +:doc:`generators/sg_v6` (arbitrary I/Q envelopes, the flagship/default +choice), :doc:`generators/sg_int4_v2` (arbitrary envelope, 4x-interpolated, +lighter-weight), :doc:`generators/sg_mixmux8` and :doc:`generators/sg_mux8` +(fixed multi-tone/CW, up to 8 simultaneous tones per channel), and the legacy +:doc:`generators/sg_v4`. Which one(s) a given board's standard firmware uses, +and how many of each, is a per-board decision -- see +:ref:`std-firmware-per-board`. + +**Signal Generator v6 (SG-v6)**, the flagship arbitrary-envelope core, has +the following features: + +* Supports uploading I/Q envelopes +* 32-bit resolution for both frequency and phase +* Configurable DDS with streaming mode (frequency can change sample-to-sample) +* 16 parallel samples per clock cycle (256-bit output bus) +* Maximum envelope length: 65536 samples per channel + +For the full generator family and which one fits which experiment, see +:doc:`generators/index`. For complete SG-v6 documentation, see +:doc:`generators/sg_v6`. + +Readout System +-------------- + +QICK ships several readout IP variants for the DDC/decimation stage -- +:doc:`readouts/readout_v2` (PYNQ-register-configured, single tone), +:doc:`readouts/readout_dynamic` (``axis_readout_v3``/``axis_dyn_readout_v1``, +tProc-configured per shot, single tone), and :doc:`readouts/readout_pfb` +(``axis_pfb_readout_v2``/``v3``/``v4``, a fixed polyphase-filter-bank +channelizer that fans one ADC input out to several simultaneous demodulated +channels). Which one(s) a given board's standard firmware uses, and how many, +is a per-board decision -- see :ref:`std-firmware-per-board`. All variants +feed the same downstream block, **Average + Buffer** (:doc:`readouts/avg_buffer`): + +**Readout Block** (architecture varies by variant -- see the pages above): + +* Digital down-conversion (DDC) +* FIR filtering +* Decimation + +**Average + Buffer Block:** + +* Raw sample storage +* Coherent averaging +* Triggered by a dedicated single-bit ``trig_N_o`` pin from the + tProcessor -- one per averaging buffer instance, *not* part of the same + channel-numbered AXI-Stream space as the readout's own configuration; see + :ref:`tproc-ports` + +For complete documentation, see :doc:`readouts/readout_v2`, :doc:`readouts/readout_dynamic`, +and :doc:`readouts/readout_pfb` (the DDC/decimation stage, in its several variants) +and :doc:`readouts/avg_buffer` (the averaged/raw buffer IP and its register map). + +The tProcessor v2 +----------------- + +The tProcessor (timing Processor) is a hard real-time co-processor inside the QICK FPGA. +It runs user-written programs that control waveform generation, data acquisition, and feedback with nanosecond precision. + +For **complete documentation**, see: + +* :doc:`/tprocv2_trm` - Full reference manual (architecture, instruction set, programming examples) +* :doc:`/topics/asmv2_cheatsheet` - Quick reference for assembly programming + +**Quick links to tProcessor topics:** + +* :ref:`tproc-quick-ref` - Most common instructions and condition codes +* :ref:`tproc-registers` - Complete register bank reference +* :ref:`tproc-examples` - Copy-paste ready code examples +* :ref:`tproc-peripherals` - ARITH (multiply), DIV, LFSR + +.. _std-firmware-per-board: + +Standard ("_std") Firmware Per Board +------------------------------------- + +QICK does not use one universal channel layout. For each supported board, +the ``qick`` firmware repository ships a **standard reference design** +("_std", under ``firmware/Top//tpv2_std/``) that provisions a +different mix of generator and readout IP suited to that board's DAC/ADC tile +count and I/O capacity. A board's *other* firmware builds (e.g. the ZCU216 +"_demo" project) can provision yet another mix again. There is no fixed +"channel N is always generator type X" rule across boards -- what follows is +each board's own layout, traced directly from that board's block-design +source (``bd_2023-1.tcl``, on branch ``feature/114-integrate-hog-fusesoc``) +rather than assumed: + +.. list-table:: + :header-rows: 1 + :widths: 18 10 12 12 10 12 12 10 12 + + * - Board (``tpv2_std`` unless noted) + - :doc:`generators/sg_v6` + - :doc:`generators/sg_int4_v2` + - :doc:`generators/sg_mixmux8` + - :doc:`generators/sg_mux8` + - :doc:`readouts/readout_dynamic` (dyn v1) + - :doc:`readouts/readout_pfb` (v4) + - :doc:`readouts/avg_buffer` + - :doc:`readouts/mr_buffer_et` + * - ZCU111 (``firmware/Top/111``) + - 8 + - 0 + - 0 + - 0 + - 4 + - 0 + - 4 + - 1 + * - RFSoC4x2 (``firmware/Top/4x2``) + - 2 + - 0 + - 0 + - 0 + - 3 + - 0 + - 2 + - 1 + * - ZCU216 (``firmware/Top/216/tpv2_std``, flagship / QICKbox) + - 4 + - 11 + - 1 + - 0 + - 2 + - 1 + - 10 + - 1 + * - ZCU216 ``tpv2_demo`` (not "_std") + - 2 + - 2 + - 1 + - 1 + - 1 (plus 1 ``axis_readout_v2`` + 1 ``axis_readout_v3``) + - 0 (1 ``axis_pfb_readout_v3`` instead) + - 7 + - 1 + +Counts are per-board instance counts of each IP core, obtained by grepping +each board's ``bd_2023-1.tcl`` for its ``create_bd_cell`` lines -- not a +per-channel numbering scheme. See :ref:`tproc-zcu216-example` below for how +the ZCU216 ``tpv2_std`` numbers above translate into an actual tProc +channel/DAC/ADC map. + +.. note:: + If using the Xilinx XM500 daughter board (ZCU111), be aware of DAC/ADC + tile filters (a physical hardware property of that daughtercard, not of + the firmware's channel numbering): + + - DAC 229 CH0/CH1: **High-pass** (1 GHz) → output ≥ 1 GHz + - DAC 229 CH2/CH3: **Low-pass** (1 GHz) → output ≤ 1 GHz + - ADC 224 CH0/CH1: **Low-pass** (1 GHz) → input ≤ 1 GHz + - DAC 228 channels: **No filters** + + This note was not re-verified against the ZCU111 ``tpv2_std`` block + design in this pass (only the ZCU216 design was traced in detail); treat + the DAC/ADC tile numbers as Xilinx's own physical labels, independent of + whichever tProc channel happens to feed them. + +.. _tproc-ports: + +tProc Waveform Ports vs. Trigger Outputs +------------------------------------------- + +In tProc v2, two architecturally separate sets of ``qick_processor`` ports +are involved in driving generators/readouts and firing triggers -- unlike +tProc v1, where a single AXIS-channel numbering space carried both (this +page previously described tProc v2 hardware using that older model; the +distinction below is the correction): + +* **Waveform/config ports** (``m0_axis``..``mN_axis``, driven by + ``WPORT_WR``/:meth:`.QickProgram.pulse`) are full AXI-Stream ports that + carry generator pulse commands and dynamic-readout per-shot configuration + words. How many exist is a synthesis parameter of ``qick_processor`` + (``CONFIG.OUT_WPORT_QTY``) -- **16** on ZCU216 ``tpv2_std``, matching its + 16 total DAC channels/generator instances (4 SG-v6 + 1 SG-mixmux8 + 11 + SG-int4-v2, per the table above). +* **Trigger outputs** (``trig_0_o``..``trig_N_o``) are independent + **single-bit** pins, each wired with a plain ``connect_bd_net`` (not an + AXI-Stream ``connect_bd_intf_net``) directly to one PMOD bit or one IP + block's ``trigger`` input. How many exist is a separate synthesis + parameter (``CONFIG.OUT_TRIG_QTY``) -- **20** on ZCU216 ``tpv2_std``. They + are not part of the same numbering space as the waveform ports, and (also + unlike tProc v1) they are not bits packed inside a generator channel's + data word either. + +Both counts were read directly off ``qick_processor_0``'s ``CONFIG.*`` +properties in the ZCU216 ``tpv2_std`` ``bd_2023-1.tcl`` this session; they +are synthesis-time parameters, so other boards' firmware can and do +instantiate different totals (verify per board rather than assuming these +numbers). + +**Full ``trig_N_o`` mapping, ZCU216 ``tpv2_std`` (verified via +``connect_bd_net`` on ``qick_processor_0/trig_*_o``):** + +.. list-table:: + :header-rows: 1 + :widths: 20 40 + + * - Trigger pin(s) + - Drives + * - ``trig_0_o`` .. ``trig_7_o`` + - ``PMOD0_0_LS`` .. ``PMOD0_7_LS`` (all 8 PMOD0 bits) + * - ``trig_8_o`` + - ``mr_buffer_et_0/trigger`` (see :doc:`readouts/mr_buffer_et`) + * - ``trig_9_o`` + - ``ddr4/trigger`` (DDR4 streaming capture) + * - ``trig_10_o`` .. ``trig_19_o`` + - ``axis_avg_buffer_0/trigger`` .. ``axis_avg_buffer_9/trigger`` -- + one dedicated trigger bit per averaging buffer instance + +This replaces the old, incomplete claim that readout triggers were "bit 14" +and "bit 15" of a channel-0 data word -- on ZCU216 ``tpv2_std`` there are 20 +independent trigger pins, only 8 of which are PMOD, and the remaining 12 +individually fire the board's 10 avg_buffers, its one ``mr_buffer_et``, and +its DDR4 capture path. A board with a different avg_buffer count (see +:ref:`std-firmware-per-board`) will have a different ``OUT_TRIG_QTY`` and a +correspondingly different mapping -- always confirm against that board's own +``bd_*.tcl`` (or, from Python, treat the driver-level trigger/arm calls as +the source of truth rather than the raw pin numbers). + +There is also a third, separate set of ports: **feedback inputs** +(``s0_axis``..``s9_axis`` on ``qick_processor_0``, ``CONFIG.IN_PORT_QTY=10`` +on ZCU216 ``tpv2_std``), each individually wired (via a per-channel clock +converter) to one averaging buffer's ``m2_axis`` live-feedback output -- +i.e. one real-time feedback input per avg_buffer, again not "2 input +channels" as this page previously claimed. + +.. _tproc-zcu216-example: + +Worked Example: ZCU216 ``tpv2_std`` +-------------------------------------- + +The ZCU216 ``tpv2_std`` design is traced here in full (it is the flagship +board and what ships in the QICKbox). All of the following was read from +``firmware/Top/216/tpv2_std/bd_2023-1.tcl`` (branch +``feature/114-integrate-hog-fusesoc``) in this session -- generator counts +match :ref:`std-firmware-per-board` above. + +**DAC side -- tProc waveform port to DAC channel:** + +The RFDC (``usp_rf_data_converter_0``) has 4 DAC tiles, 4 channels each +(``s00``..``s33``, 16 channels total). ``qick_processor_0``'s core clock +(``t_clk_i``) is tied to DAC tile 2's sample clock; SG-int4-v2 and +SG-mixmux8 (tiles 1-3) run on that same clock, so their tProc ports connect +straight through. DAC tile 0 (the 4 SG-v6 channels) runs on its own, +independent sample clock, so those 4 ports cross through a +clock-domain-crossing block (``axis_cdcsync_v1_0``, RTL parameter ``N=4``) +first. + +.. list-table:: + :header-rows: 1 + :widths: 12 18 20 20 15 + + * - tProc port + - CDC/demux stage + - Generator instance + - Generator type + - DAC tile:ch + * - ``m0_axis`` + - ``axis_cdcsync_v1_0`` (4-ch CDC, core clk → DAC-tile-0 clk) + - ``axis_signal_gen_v6_0`` + - SG-v6 + - tile 0 : 0 + * - ``m1_axis`` + - ``axis_cdcsync_v1_0`` + - ``axis_signal_gen_v6_1`` + - SG-v6 + - tile 0 : 1 + * - ``m2_axis`` + - ``axis_cdcsync_v1_0`` + - ``axis_signal_gen_v6_2`` + - SG-v6 + - tile 0 : 2 + * - ``m3_axis`` + - ``axis_cdcsync_v1_0`` + - ``axis_signal_gen_v6_3`` + - SG-v6 + - tile 0 : 3 + * - ``m4_axis`` + - ``axis_tmux_v1_0`` (address-tagged 3-way demux; direct to + generator, or via ``axis_cdcsync_v1_2`` for the other two) + - ``axis_sg_mixmux8_v1_0`` + - SG-mixmux8 + - tile 1 : 0 + * - ``m5_axis`` + - direct (same clock domain as tProc core) + - ``axis_sg_int4_v2_0`` + - SG-int4-v2 + - tile 1 : 1 + * - ``m6_axis`` + - direct + - ``axis_sg_int4_v2_1`` + - SG-int4-v2 + - tile 1 : 2 + * - ``m7_axis`` + - direct + - ``axis_sg_int4_v2_2`` + - SG-int4-v2 + - tile 1 : 3 + * - ``m8_axis`` + - direct + - ``axis_sg_int4_v2_3`` + - SG-int4-v2 + - tile 2 : 0 + * - ``m9_axis`` + - direct + - ``axis_sg_int4_v2_4`` + - SG-int4-v2 + - tile 2 : 1 + * - ``m10_axis`` + - direct + - ``axis_sg_int4_v2_5`` + - SG-int4-v2 + - tile 2 : 2 + * - ``m11_axis`` + - direct + - ``axis_sg_int4_v2_6`` + - SG-int4-v2 + - tile 2 : 3 + * - ``m12_axis`` + - direct + - ``axis_sg_int4_v2_7`` + - SG-int4-v2 + - tile 3 : 0 + * - ``m13_axis`` + - direct + - ``axis_sg_int4_v2_8`` + - SG-int4-v2 + - tile 3 : 1 + * - ``m14_axis`` + - direct + - ``axis_sg_int4_v2_9`` + - SG-int4-v2 + - tile 3 : 2 + * - ``m15_axis`` + - direct + - ``axis_sg_int4_v2_10`` + - SG-int4-v2 + - tile 3 : 3 + +Every ``qick_processor_0/mN_axis`` port passes through a ``sg_translator`` +instance before reaching its generator (18 ``sg_translator`` instances exist +in total: 16 feed the generators above, ``sg_translator_16``/``_17`` feed the +two dynamic readouts' config ports instead -- see below). Register slices +(``axis_register_slice_0``-``_7``) also sit inline on the SG-v6 path, but +these stay on one clock (they don't add a second CDC hop) -- they're +ordinary pipeline stages for timing closure, confirmed by both sides sharing +the same clock net in the .tcl. + +``m4_axis`` is the one port that isn't a clean 1-generator mapping: RTL for +``axis_tmux_v1`` (``firmware/ip/axis_tmux_v1/src/tmux.sv``) shows it demuxes +one AXI-Stream input to one of *N* outputs by an address tag carried in the +data word itself (the top 8 bits), not a fixed per-port assignment. On this +board (``CONFIG.N=3``), that lets tProc channel 4 carry pulse commands for +the SG-mixmux8 generator *and* per-shot config words for both dynamic +readouts, distinguished by that tag -- one of the demuxed outputs goes +straight to ``sg_translator_4`` (mixmux8, same clock domain), and the other +two cross into the ADC-tile-2 clock domain via a second CDC block, +``axis_cdcsync_v1_2`` (``N=2``), before reaching ``sg_translator_16``/``_17`` +and then ``axis_dyn_readout_v1_0``/``_1``'s ``s0_axis`` config ports. + +**On the CDC blocks' purpose:** ``axis_cdcsync_v1`` (RTL: +``firmware/ip/axis_cdcsync_v1/src/cdcsync.sv``) exposes independent +input-side and output-side clock/reset pairs for up to ``N`` parallel +same-width streams -- it is a clock-domain-crossing bridge, confirmed by +tracing each instance's ``aclk`` nets in this board's .tcl (input side tied +to the tProc core clock, output side tied to the destination tile's own +clock). It is **not** part of the ``xcom`` multi-board synchronization +mechanism (:doc:`/topics/xcom`) despite the similar name -- that is a wholly +separate IP block with its own network/serial interface. Both CDC instances +on this board exist purely because DAC tile 0 and the ADC-tile-2-clocked +readout logic run on clocks independent of the tProc's own core clock, not +for any cross-board purpose. + +**ADC side -- readout instances:** + +* ``axis_dyn_readout_v1_0`` -- ADC input from tile 2, channel 0 + (``usp_rf_data_converter_0/m20_axis``). +* ``axis_pfb_readout_v4_0`` -- ADC input from tile 2, channel 1 + (``m21_axis``); channelizes into 8 outputs, ``m0_axis``..``m7_axis``. +* ``axis_dyn_readout_v1_1`` -- ADC input from tile 2, channel 2 + (``m22_axis``). + +Each dynamic readout's decimated output (``m1_axis``) and each of the PFB +readout's 8 channelized outputs feed their own ``axis_broadcaster``, which +fans out to (a) one dedicated ``axis_avg_buffer`` (1:1 -- 10 total: 2 for the +dynamic readouts + 8 for the PFB readout's 8 channels) and (b) a shared +10-input AXI-Stream switch (``axis_switch_ddr``) feeding the single DDR4 +capture path, letting software pick which one of the 10 channels streams to +DDR4. Separately, the two dynamic readouts' *undecimated* outputs +(``m0_axis``) feed a 2-input switch (``axis_switch_mr``) into the single +shared ``mr_buffer_et_0`` instance. This matches, and was cross-checked +against, the independently-traced fan-out in :doc:`readouts/readout_pfb`'s +":ref:`readout-pfb-zcu216`" section. + +Checking Your Own Board's Configuration +------------------------------------------ + +Every table on this page describes one specific board's firmware build, +traced from that build's source. Your board may be a different one, or the +same board with a different firmware build -- do not assume generator +counts, readout counts, or channel/trigger mappings from this page apply to +your setup. Instead, read them from the running system: + +* ``soc.gens`` -- list of generator drivers, in board-config order (index + is *not* guaranteed to equal the tProc waveform-port number -- see + :ref:`tproc-zcu216-example` for why that mapping can be non-trivial). +* ``soc.readouts`` -- list of readout drivers (mix of + :doc:`readouts/readout_v2`/:doc:`readouts/readout_dynamic`/:doc:`readouts/readout_pfb` types, + board-dependent). +* ``soc.avg_bufs`` -- list of :doc:`readouts/avg_buffer` drivers. +* ``print(soccfg)`` -- full human-readable dump of the board's configuration, + including per-channel generator/readout types and every buffer's + ``avg_maxlen``/``buf_maxlen``. + +Timing +------ + +- **FPGA clock:** 384 MHz → period = 2.6 ns +- **DAC speed:** 384 × 16 = 6144 MHz → resolution ~163 ps +- **ADC speed:** 384 × 8 = 3072 MHz, decimated by 8 → resolution ~2.6 ns +- **Minimum DAC pulse length:** 16 samples (shorter pulses can be zero-padded) + +Firmware Parameters +------------------- + +.. note:: + This table previously carried tProc-**v1**-era figures (8k-instruction + program memory, 256-sample stack, 16-bit gain) copied into this page + unchanged. tProc v2's real numbers, confirmed against + :doc:`/tprocv1` and :doc:`/tprocv2_trm` in this session, are quite + different -- notably the **stack is 8 entries deep, not 256**, and + program/data memory sizes are synthesis parameters + (``PMEM_AW``/``DMEM_AW``), not a fixed "8k"/"4096". As with everything + else on this page, treat board-specific sizes as something to read from + ``soccfg`` (or ``soc.tproc.cfg``), not to assume. + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Value + * - Pulse memory length (per SG-v6) + - 65536 samples ×2 (I/Q) = 128k total (``N=16`` default -- see + :doc:`generators/sg_v6`) + * - Decimated ADC buffer + - Board-dependent, ``2**N_BUF`` samples per component (I,Q) -- see + :doc:`readouts/avg_buffer` + * - Accumulated ADC buffer + - Board-dependent, ``2**N_AVG`` samples per component (I,Q) -- see + :doc:`readouts/avg_buffer` + * - tProc v2 program/data memory + - Synthesis parameters (``PMEM_AW``/``DMEM_AW``), board-dependent -- + see :doc:`/tprocv2_trm` + * - tProc v2 stack size + - **8** entries (fixed, not a synthesis parameter) -- see + :doc:`/tprocv2_trm` + * - Phase conversion + - :math:`\Delta \phi = 2\pi/2^{32}` or :math:`360/2^{32}` degrees + * - Gain range (tProc v2 / ``w_gain``) + - 32-bit signed -- verified against RTL (``qcore_reg_bank.sv``) and + ``asm_v2.py``'s ``Waveform.compile()`` in :doc:`/tprocv2_trm` + +Related Documentation +--------------------- + +* :doc:`/tprocv2_trm` - Complete tProcessor v2 reference manual +* :doc:`generators/index` - Full generator-core family (SG-v6, SG-int4-v2, SG-mixmux8, SG-mux8) +* :doc:`generators/sg_v6` - Signal Generator v6 documentation +* :doc:`readouts/index` - Readout system overview +* :doc:`readouts/readout_dynamic` - tProc-configured single-tone readouts (axis_readout_v3 / axis_dyn_readout_v1) +* :doc:`readouts/readout_pfb` - Polyphase-filter-bank channelized readouts (axis_pfb_readout_v2/v3/v4) +* :doc:`readouts/avg_buffer` - Averaged/raw buffer IP downstream of every readout variant +* :doc:`readouts/mr_buffer_et` - Multi-rate buffer (DDR4/streaming) shared readout path +* :doc:`/topics/asmv2_cheatsheet` - tProc v2 assembly quick reference +* :doc:`/tutorials/README` - tProc v2 tutorial examples +* `Signal Generator v6 source `_ diff --git a/docs/source/firmware/readouts/avg_buffer.rst b/docs/source/firmware/readouts/avg_buffer.rst new file mode 100644 index 000000000..68ab819c0 --- /dev/null +++ b/docs/source/firmware/readouts/avg_buffer.rst @@ -0,0 +1,237 @@ +======================================================== +Averager + Buffer (axis_avg_buffer) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +The **Averager + Buffer** (``axis_avg_buffer``) is the IP block that sits +downstream of a readout (:doc:`index`) 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 (:meth:`.AcquireMixin.acquire`) and raw decimated +waveforms (:meth:`.AcquireMixin.acquire_decimated`) -- see +:doc:`/topics/readout_modes` 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 +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``B`` + - 16 + - Bit width of one I or one Q sample coming in on ``s_axis`` + (``s_axis_tdata`` is ``2*B`` bits: I in bits ``[B-1:0]``, Q in bits + ``[2*B-1:B]``). + * - ``N_AVG`` + - 10 + - Address width of the averaged-buffer BRAM. Depth (max number of + accumulated points that can be stored per trigger burst) is + ``2**N_AVG``. Exposed to Python as ``cfg['avg_maxlen']``. + * - ``N_BUF`` + - 10 + - Address width of the raw-buffer BRAM. Depth (max number of raw + samples per trigger) is ``2**N_BUF``. Exposed to Python as + ``cfg['buf_maxlen']``. + +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 :ref:`avgbuf-python`). 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 += I`` and ``acc_q += Q`` + (plain running sums, no length normalization in hardware -- that division + happens in software, see :meth:`.AcquireMixin.acquire`'s length + normalization). After ``AVG_LEN_REG`` samples, the accumulated + ``{acc_q, acc_i}`` word is written to BRAM at ``AVG_ADDR_REG`` and 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 the ``EDGE_COUNTER`` + feature the Python driver flags via ``cfg['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 :meth:`.AcquireMixin.acquire_decimated` +returns. + +-------------------------------------------------------------------- +5. Register Map +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Register + - Width + - Description + * - ``AVG_START_REG`` + - 1 bit + - 0: averager disabled. 1: armed, waiting for/capturing on ``trigger``. + * - ``AVG_ADDR_REG`` + - ``N_AVG`` bits + - BRAM write address for the next accumulated point. + * - ``AVG_LEN_REG`` + - 32 bits + - Number of input samples to accumulate before writing out one point. + * - ``AVG_PHOTON_MODE_REG`` + - 1 bit + - 0: accumulate I/Q. 1: edge-counter mode (see above). + * - ``AVG_H_THRSH_REG`` / ``AVG_L_THRSH_REG`` + - ``B`` bits (signed) + - High/low thresholds for edge-counter mode. + * - ``AVG_DR_START_REG`` + - 1 bit + - 1: stream accumulated data out on ``m0_axis``. + * - ``AVG_DR_ADDR_REG`` / ``AVG_DR_LEN_REG`` + - ``N_AVG`` bits + - Start address / sample count for the ``m0_axis`` read-back. + * - ``BUF_START_REG`` + - 1 bit + - 0: raw buffer disabled. 1: armed, waiting for/capturing on ``trigger``. + * - ``BUF_ADDR_REG`` / ``BUF_LEN_REG`` + - ``N_BUF`` bits + - BRAM write address / number of raw samples to capture. + * - ``BUF_DR_START_REG`` + - 1 bit + - 1: stream raw data out on ``m1_axis``. + * - ``BUF_DR_ADDR_REG`` / ``BUF_DR_LEN_REG`` + - ``N_BUF`` bits + - Start address / sample count for the ``m1_axis`` read-back. + +.. _avgbuf-python: + +-------------------------------------------------------------------- +6. Python Interface +-------------------------------------------------------------------- + +Most QICK programs never call this driver directly -- :meth:`.AcquireMixin.acquire` +and :meth:`.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]``. + +.. code-block:: python + + 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 :meth:`.AcquireMixin._average_buf` and +:meth:`.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. + +Related Documentation +---------------------- + +* :doc:`index` -- the readout block that feeds this buffer's ``s_axis``. +* :doc:`/topics/readout_modes` -- ``acquire()`` vs. ``acquire_decimated()``. +* :doc:`/tprocv2_trm` -- the tProcessor trigger that starts a capture, and + the m2_axis feedback port. +* :doc:`mr_buffer_et` -- the alternate multi-rate buffer used for + DDR4/streaming readout of the *undecimated* data stream, as opposed to + this block's decimated raw buffer. diff --git a/docs/source/firmware/readouts/index.rst b/docs/source/firmware/readouts/index.rst new file mode 100644 index 000000000..fd5f4b9d3 --- /dev/null +++ b/docs/source/firmware/readouts/index.rst @@ -0,0 +1,189 @@ +Readout System - QICK Firmware +============================== + +.. contents:: + :local: + :depth: 2 + +.. toctree:: + :maxdepth: 2 + :caption: 6. Readout System + :hidden: + + readout_v2 + readout_dynamic + readout_pfb + avg_buffer + mr_buffer_et + kidsim + +Overview +-------- + +The QICK readout system is built around two main IP blocks: + +* **Readout Block** - Digital down-conversion (DDC), filtering, and decimation +* **Average + Buffer Block** - Data accumulation and storage + +The readout process is triggered by a dedicated single-bit ``trig_N_o`` pin +from the tProcessor (one per averaging buffer instance -- see +:ref:`tproc-ports` in :doc:`../index` for the full pin model) and can be +configured for raw sample capture or coherent averaging. + +.. figure:: ../../../../graphics/qsystem-readout.svg + :width: 100% + :align: center + +Readout Block +------------- + +The readout block performs: + +1. **Digital Down-Conversion (DDC)** - Mixes the ADC input with a DDS tone +2. **FIR Filtering** - Anti-aliasing filter +3. **Decimation by 8** - Reduces sample rate for easier processing + +**Key parameters:** + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Parameter + - Description + * - DDS frequency + - Configured via register (not intended for real-time hopping) + * - FIR taps + - Fixed coefficients (compiled into firmware) + * - Decimation factor + - Fixed at 8 + * - Input selection + - Can route raw input, DDS wave, or frequency-shifted signal + +The user can select which signal is sent to the FIR stage using an output selection register. + +Average + Buffer Block +---------------------- + +This block receives the decimated data stream and can: + +* **Store raw samples** - Capture individual samples for analysis +* **Perform coherent averaging** - Accumulate multiple acquisitions + +**Capabilities:** + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Parameter + - Value + * - Raw buffer length + - Board-dependent, ``2**N_BUF`` samples per component (I/Q) -- see + :doc:`avg_buffer` + * - Accumulated buffer length + - Board-dependent, ``2**N_AVG`` samples per component (I/Q) -- see + :doc:`avg_buffer` + * - Trigger source + - One dedicated ``trig_N_o`` pin per buffer instance, *not* a bit + within a channel-numbered data word -- see :ref:`tproc-ports` in + :doc:`../index` + +The buffer IP that stores/accumulates this data (registers, DMA transfer, +edge-counter mode) is documented separately in :doc:`avg_buffer`. + +Python Interface +---------------- + +Normal QICK programs do **not** poke the readout/buffer drivers directly. +The workflow, inside a :class:`.QickProgram` (e.g. an +:class:`.AveragerProgramV2` subclass), is: + +1. Declare each readout channel you'll use, once, with + :meth:`.QickProgram.declare_readout` -- this sets the downconversion + frequency/phase/output-select and the readout window length. +2. Fire the ADC capture at the right point in your sequence with + :meth:`.QickProgramV2.trigger` (or the tProc v1 equivalent). +3. After running the program, pull the data back with + :meth:`.AcquireMixin.acquire` (accumulated) or + :meth:`.AcquireMixin.acquire_decimated` (raw waveform) -- see + :doc:`/topics/readout_modes` for which one to use. + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + class LoopbackProgram(AveragerProgramV2): + def _initialize(self, cfg): + # readout channel 0: 100 MHz downconversion, 3 us window + self.declare_readout(ch=0, length=3.0, freq=100.0, gen_ch=0) + self.declare_gen(ch=0, nqz=1) + self.add_pulse(ch=0, name="probe", style="const", + freq=100.0, phase=0, gain=0.5, length=3.0) + + def _body(self, cfg): + self.trigger(ros=[0], t=0) # arm readout channel 0 + self.pulse(ch=0, name="probe", t=0) + self.delay_auto(t=1.0) # wait for pulse+readout to finish before looping + + prog = LoopbackProgram(soccfg, reps=1000, final_delay=10.0, cfg={}) + + # Accumulated: one (I, Q) point per rep, averaged + iq_avg = prog.acquire(soc) + + # Decimated: the raw readout waveform (for debugging the window/pulse) + prog2 = LoopbackProgram(soccfg, reps=1, final_delay=10.0, cfg={}) + iq_dec = prog2.acquire_decimated(soc) + +**Low-level driver access:** + +The readout and buffer are still reachable directly, as ``soc.readouts[i]`` +and ``soc.avg_bufs[i]``, for debugging or hand-rolled acquisition outside +the ``acquire()``/``acquire_decimated()`` flow -- see :doc:`avg_buffer`'s +Python Interface section for the buffer-level ``config_avg``/ +``transfer_avg``/``config_buf``/``transfer_buf`` calls this normally sits on +top of. :meth:`.AbsReadout.set_all`/``set_all_int`` on the readout driver +itself are explicitly for debugging only -- the recommended way to configure +a readout's frequency/phase/output-select is ``declare_readout()``. + +Hardware Considerations +----------------------- + +**Clock frequencies:** + +- ADC sampling rate: ``soc.fs_adc`` (typically 3072 MHz) +- After decimation by 8: effective rate = ``soc.fs_adc / 8`` + +**Buffer sizes:** + +- Raw mode: up to ``soc.cfg['readouts'][i]['buf_maxlen']`` I/Q pairs (1024 on + the reference board, i.e. 2k total samples) +- Average mode: up to ``soc.cfg['readouts'][i]['avg_maxlen']`` I/Q pairs + (16384 on the reference board, i.e. 32k total) -- read these from your own + board's config rather than assuming a number, see :doc:`avg_buffer`. + +**Example: Calculating acquisition time** + +.. code-block:: python + + n_samples = 1024 + decimation = 8 + adc_rate = soc.fs_adc # Hz + sample_period = 1 / (adc_rate / decimation) # seconds per decimated sample + acquisition_time = n_samples * sample_period + print(f"Acquisition takes {acquisition_time * 1e6:.2f} us") + +Related Documentation +--------------------- + +* :doc:`avg_buffer` - the averaged/raw buffer IP downstream of this readout +* :doc:`readout_pfb` - the polyphase-filter-bank multi-channel readout + variant (``axis_pfb_readout_v2``/``v3``/``v4``), which channelizes into + several simultaneous demod outputs instead of one tunable frequency +* :doc:`/topics/readout_modes` - ``acquire()`` vs. ``acquire_decimated()`` +* :doc:`/tprocv2_trm` - tProcessor v2 for triggering and sequencing +* :doc:`../index` - Firmware overview and channel assignments +* :doc:`/topics/timing` - Timing considerations for readout +* :doc:`/topics/freq_matching` - Frequency matching between generators and readouts diff --git a/docs/source/firmware/readouts/kidsim.md b/docs/source/firmware/readouts/kidsim.md new file mode 100644 index 000000000..e4cae4645 --- /dev/null +++ b/docs/source/firmware/readouts/kidsim.md @@ -0,0 +1,483 @@ +# KID / Qubit Resonator Emulator (axis_kidsim_v3) + +```{contents} +:local: +:depth: 2 +``` + +```{note} +This page documents the **fusesoc-ported** `axis_kidsim_v3` (register model +and driver under `firmware/fusesoc/cores/ip/axis_kidsim_v3/`), which was +still in progress and not yet merged at the time this page was written. A +legacy Vivado-packaged version of the same algorithm exists at +`firmware/ip/axis_kidsim_v3/`; the RTL (`kidsim`/`kidsim_top`) is the same +design, but that legacy IP has no `qick_lib` driver. +``` + +**axis_kidsim_v3** is not a readout -- it's a per-lane RF resonator +simulator, used to test KID/qubit readout firmware and software against a +physically-realistic resonator response without a real cryostat. Each lane +demodulates its input with a DDS carrier, applies a 1-pole/1-zero IIR at +baseband (the resonator response), and remodulates back to RF. The module +lives at `firmware/rtl_lib/ip/axis_kidsim_v3.sv` (top level) / +`firmware/rtl_lib/ctrl/kidsim_top.sv` / `kidsim.sv` (per-lane core), and is +exposed to Python through `AxisKidsimV3` +(`firmware/fusesoc/cores/ip/axis_kidsim_v3/driver/axis_kidsim_v3.py`). + +## 1. General Description + +`axis_kidsim_v3` wraps `L` parallel `kidsim_top` lanes behind one AXI4-Lite +register block. Each lane processes one 32-bit {Q, I} sub-word of the packed +AXI-Stream input (`s_axis_tdata` is `32*L` bits total). Per-lane +configuration (DDS/IIR/output-select/puncturing registers) is written +through a shared register set and latched into the target lane by writing +`ADDR_REG` (the lane index) then pulsing `WE_REG`. + +```{figure} /images/firmware/kidsim-blocks.svg +:align: center +:width: 90% + +One representative `kidsim_top` lane (of `L`), matching `kidsim_top.sv`'s +own documented datapath: `punct` selects one sample per frame, `kidsim` +does the demodulate-by-DDS / IIR-filter / remodulate resonator +simulation, and the output mux falls back to a delay-matched bypass of +the raw input when that lane's sample isn't the punctured one. +``` + +## 2. Synthesis Parameters + +```{list-table} +:header-rows: 1 +:widths: 20 15 65 + +* - Parameter + - Default + - Description +* - `L` + - 8 + - Number of parallel lanes (independent simulated resonators). +``` + +## 3. Register Map + +All registers are 32-bit, byte-addressed, `sw=rw`/`hw=r` (software writes, +hardware reads) unless noted. From +`firmware/fusesoc/cores/ip/axis_kidsim_v3/rdl/axis_kidsim_v3_regmap.rdl`. + +```{list-table} +:header-rows: 1 +:widths: 20 10 15 55 + +* - Register + - Addr + - Width + - Description +* - `DDS_BVAL_REG` + - 0x00 + - 16 (signed Q15) + - Chirp ramp start value B. `ramp(n) = B - n*M` for `n` in `[0, N]`. +* - `DDS_SLOPE_REG` + - 0x04 + - 16 (signed Q15) + - Chirp ramp slope M per step. +* - `DDS_STEPS_REG` + - 0x08 + - 16 + - Number of chirp ramp steps N. +* - `DDS_WAIT_REG` + - 0x0C + - 16 + - Wait cycles per chirp step W (0 = one step/cycle). +* - `DDS_FREQ_REG` + - 0x10 + - 16 + - DDS base phase increment (carrier frequency); `pinc(n) = FREQ - ramp(n)`. +* - `IIR_C0_REG` + - 0x14 + - 16 (signed Q15) + - IIR feed-forward (zero) coefficient C0. See §4 for the physical meaning. +* - `IIR_C1_REG` + - 0x18 + - 16 (signed Q15) + - IIR feed-back (pole) coefficient C1 -- sets the resonator linewidth. +* - `IIR_G_REG` + - 0x1C + - 16 (signed Q15) + - IIR output gain G (`0x7FFF` ~= 1.0). +* - `OUTSEL_REG` + - 0x20 + - 2 + - Output select: 0=processed signal (demod/IIR/remod), 1=DDS carrier only, 2=input bypass (default), 3=zero. +* - `PUNCT_ID_REG` + - 0x24 + - 16 + - Puncturing: which sample index within a frame is selected for output. +* - `ADDR_REG` + - 0x28 + - 8 + - Target lane index for the next configuration write. +* - `WE_REG` + - 0x2C + - 1 + - Write enable -- pulse 1 (with `ADDR_REG` already set) to latch the above registers into that lane. Latches all registers together: a frequency-only update must rewrite `C0`/`C1`/`G` with their existing values too, or it will perturb the resonator. +``` + +Pass-through configuration (no filtering, unity gain): `C0=0`, `C1=0`, +`G=0x7FFF`. + +## 4. Python Interface + +`AxisKidsimV3` (`bindto = ['qick:ip:axis_kidsim_v3:1.0']`) is a thin, +PeakRDL-generated register-model wrapper -- as of this writing it exposes +only raw register access via `.regs` (e.g. `ip.regs.IIR_C1_REG.C1.write(...)`, +matching the field names in the register map above), with the file's +`# -- hand-written high-level methods below --` marker left as a +placeholder for higher-level convenience methods (e.g. a +"configure resonator" helper) not yet written. There is also a +standalone Python behavioral model, `model/kidsim_resonator.py` (with an +accompanying `model/kidsim_resonator_demo.ipynb`), useful for prototyping +resonator parameters offline before writing them to hardware. + +## 5. Resonator Physics -- Design Note + +The rest of this page is the design note for the emulator's physical model: +what the IIR/DDS register values correspond to in terms of resonator +properties (linewidth, Q, coupling), what physical readout topology it +implements, and how a mid-readout ground/excited frequency jump behaves. + +**Modules:** `kidsim_top` → `kidsim` → (`dds_chirp`, `cmult`, `iir_iq` → +`iir_1p1z`) + +### 5.1. What the emulator does + +Each lane simulates one resonator by **demodulating** the incoming stream +with a DDS carrier, **filtering** the resulting complex baseband with a +first-order (1-pole/1-zero) IIR, and **remodulating** back to RF: + +``` +din --[din_la = DDS_LATENCY]---------------------\ + prod0 = din . e^{-jf} (CONJB=1, demod) +dds_chirp --> e^{jf(t)} ---------------------------/ + | | + | iir_iq (H(z), per I and Q) + | | + \--[dds_dout_la = 8]--> e^{jf} -----------------> prod1 = iir . e^{+jf} (remod) +``` + +The IIR runs on the **demodulated** (baseband) signal, so its passband is +centred at DC in the rotating frame. Remodulation shifts that passband back +up to the carrier frequency $f_c$. The net effect from `din` to `prod1` is +therefore a **band-pass resonator centred at the carrier**, with its shape +set by the IIR coefficients. + +The IIR itself (`iir_1p1z`) is Direct-Form I: + +$$H(z) = G\,\frac{1 - C_0 z^{-1}}{1 - C_1 z^{-1}}$$ + +with $C_0, C_1, G$ signed $Q(B-1)$ in $[-1, +1)$ and a real pole at $z = C_1$. +`iir_iq` applies this **independently** to the I and Q components with the +same real coefficients, so in the rotating frame it is a complex-baseband +filter with a **real** pole sitting at baseband DC. + +**Key structural fact for everything below:** the resonant frequency is set +by the *carrier* (`DDS_FREQ_REG`), **not** by the pole. The pole (`C1`) sets +the linewidth; the zero (`C0`) sets the coupling; the gain (`G`) sets the +overall scale. Switching "ground" <-> "excited" is a change of +`DDS_FREQ_REG`. + +### 5.2. From baseband filter to band-pass resonator + +Let the carrier phase be $\varphi[n]$ (for a CW tone, $\varphi[n] = \omega_c n$, +$\omega_c = 2\pi f_c/f_s$). Demod multiplies by $e^{-j\varphi}$, the IIR +applies $H(e^{j\omega})$ at baseband, remod multiplies by $e^{+j\varphi}$. +For a CW carrier the composite response from input to output is a +**frequency-shifted** copy of the baseband filter: + +$$H_\text{bp}(e^{j\omega}) = H\!\left(e^{j(\omega - \omega_c)}\right)$$ + +So the low-pass $H$ (real pole near DC) becomes a band-pass centred at +$\omega_c$. All the resonator physics is in $H$; the carrier only says +*where* the resonance sits. + +### 5.3. Scale factors -- IIR/DDS registers -> resonator properties + +Let $f_s$ be the sample rate at the IIR, $N$ the DDS phase-accumulator +width, and write the pole as $C_1 = 1 - \varepsilon$ with $\varepsilon \ll 1$ +(high-Q regime). + +```{list-table} +:header-rows: 1 +:widths: 30 40 30 + +* - Resonator property + - Expression + - Set by +* - Resonant frequency + - $f_0 = f_c = \dfrac{\text{DDS\_FREQ\_REG}}{2^{N}}\,f_s$ + - `DDS_FREQ_REG` (carrier) +* - Field decay rate + - $\kappa/2 = (1 - C_1)\,f_s$ + - `C1` (pole) +* - Energy decay rate (loaded linewidth) + - $\kappa = 2(1 - C_1)\,f_s$ + - `C1` +* - FWHM (Hz) + - $\dfrac{\kappa}{2\pi} = \dfrac{(1 - C_1)\,f_s}{\pi}$ + - `C1` +* - Loaded quality factor + - $Q_L = \dfrac{\pi f_0}{(1 - C_1)\,f_s}$ + - `C1`, `DDS_FREQ_REG` +* - Ringdown time (field) + - $\tau = \dfrac{1}{(1 - C_1)\,f_s}$ + - `C1` +* - On-resonance response + - $H(1) = G\,\dfrac{1 - C_0}{1 - C_1}$ + - `G`, `C0`, `C1` +``` + +**Derivation of the linewidth.** The pole's impulse response is +$\propto C_1^{\,n} = e^{n\ln C_1} \approx e^{-\varepsilon n}$ for +$C_1 = 1-\varepsilon$. In continuous time ($t = n/f_s$) the field envelope +decays as $e^{-\varepsilon f_s t} = e^{-\kappa t/2}$, giving +$\kappa/2 = (1-C_1)f_s$. The power spectrum of a field decaying at $\kappa/2$ +is a Lorentzian of FWHM $\kappa$ (rad/s), i.e. $(1-C_1)f_s/\pi$ in Hz, and +$Q_L = \omega_0/\kappa = 2\pi f_0/\kappa$ follows. The $(1-C_1)$ form is the +high-$Q$ limit; the **exact** per-clock decay is $|y_1|[n]\propto C_1^{\,n}$ +(xsim-confirmed), so $\kappa = -2\ln(C_1)\,f_s$ exactly. At $C_1=0.9$ the two +differ by ~5%; for any realistic high-$Q$ resonator ($C_1\to1$) they +coincide. + +> **Sanity check against the code.** The header notes numerical fidelity +> measured with `C0=0, C1=0, G=32767` (~=0.99997). That is the *pass-through* +> limit: pole at DC, no filtering, $H(1) = G(1-0)/(1-0) = G \approx 1$. The +> residual ~3 LSB error is the $G = 32767/32768 \ne 1$ offset plus IIR +> truncation and the two `cmult` round stages -- consistent with the table. + +### 5.4. What physical configuration this is + +The 1-pole/1-zero shape is exactly the response of a **single-mode resonator +in a notch / hanger geometry** (equivalently a reflection measurement). The +standard hanger transmission is + +$$S_{21}(\Delta) = 1 - \frac{\kappa_c/2}{j\Delta + \kappa/2}, \qquad \Delta = \omega - \omega_0,\ \ \kappa = \kappa_i + \kappa_c,$$ + +which is a bilinear (Mobius) function of frequency -- precisely a +1-pole/1-zero rational form. Rearranging, + +$$S_{21}(\Delta) = \frac{j\Delta + \kappa_i/2}{j\Delta + \kappa/2},$$ + +so on resonance $S_{21}(0) = \kappa_i/\kappa$ (notch depth) and far off +resonance $S_{21} \to 1$. This is the readout topology used in cQED -- so +**the current IIR is already the right physical model**, not an +approximation to be replaced. + +Matching $H(z)$ to $S_{21}$ at baseband DC ($z=1$) gives a clean coupling +interpretation: + +```{list-table} +:header-rows: 1 +:widths: 25 75 + +* - Filter quantity + - Physical meaning +* - $1 - C_1$ + - total (loaded) linewidth $\kappa = \kappa_i + \kappa_c$ +* - $1 - C_0$ + - internal loss $\kappa_i$ +* - $C_0 - C_1$ + - coupling $\kappa_c$ +* - $\dfrac{1-C_0}{1-C_1}$ + - notch depth $\kappa_i/\kappa$ (1 = shallow/undercoupled, 1/2 = critical, ->0 = deep/overcoupled) +* - $G$ + - off-resonance baseline (set ~= 1 for a passive notch) +``` + +So today's `C0 = 0` means $\kappa_i = \kappa$ -- i.e. **fully +internally-limited, no external coupling contrast** (the "resonator" has no +notch, it's just the pole). To get a realistic notch you set $C_0$ **near** +$C_1$, with the small gap $C_0 - C_1$ encoding $\kappa_c$. + +**Where a more realistic IIR would help** (candidates for v4, in priority +order): + +1. **Compute coefficients from physics, don't hand-tune.** Given target + $\kappa_i, \kappa_c, \omega_0$, derive $C_0, C_1, G$ via a bilinear (or + matched-z) transform of $S_{21}$. This makes the register values mean + something and makes ground/excited a physics parameter set. +2. **Complex gain for Fano asymmetry.** Real resonators show an asymmetric, + rotated notch (impedance mismatch / cable delay -- the $a\,e^{j\phi}$ + prefactor in the Khalil notch model). A real `G` cannot produce it; a + complex gain (an I/Q rotation) can. There is already a `cmult` in the + chain to fold this into. +3. **Warping is negligible.** Because the resonator bandwidth $\ll f_s$, + bilinear frequency warping is not worth correcting; the real-pole-at- + baseband-DC discretisation is fine. + +### 5.5. Ground/excited switching and the stored field + +The physically interesting question: *when the resonator frequency jumps +mid-readout, there is stored energy with a definite phase -- is it handled +correctly?* + +**Where the stored field lives.** The intracavity field envelope is the IIR +feedback state. In each `iir_1p1z` it is `y1` (the saturated pole state +`y1_r1a`/`y1_r1b`); the I and Q cores together hold the **complex** +intracavity envelope, expressed in the frame rotating at the carrier. + +**The jump mechanism.** A jump changes the carrier frequency (`dds_freq_r` +-> `pinc`). The pole `C1` is unchanged, so the linewidth $\kappa$ is +preserved -- correct: the cavity's loss rate doesn't change when its +frequency does. Because the pole sits at baseband DC in *both* the old and +new frames, the stored field's ringing frequency snaps to the new +$\omega_0$ with no transient *in the frame*, the correct linear-resonator +behaviour (the eigenfrequency is a cavity property and changes instantly; +the field amplitude and phase are continuous). + +**The three correctness conditions, now checked against the RTL:** + +1. **DDS phase continuity -- SATISFIED by construction.** `dds_phase_ctrl` + emits the control word `{phase=0, pinc}` with `pinc = FREQ_REG - ramp(n)` + and a *constant* phase-offset field of 0. The phase **accumulator lives + inside the DDS IP** (`dds_phasegen_sincos_ow16_lat8`) and integrates the + streamed `pinc`; changing `FREQ_REG` changes the *increment* (the slope), + not the accumulated value, so the carrier phase is continuous across the + change -- the lab-frame field cannot teleport. The controller also + advances its datapath only on `i_din_valid`, so bubbles hold the + sequence rather than jumping it. *(One residual check: eyeball the DDS + behavioural model to confirm the accumulator is free-running and never + reloaded -- the streaming-`pinc` / fixed-`POFF=0` control word is exactly + the phase-continuous configuration, so this is expected.)* +2. **IIR state preserved -- SATISFIED.** The `y1` pole registers reset only + on `~i_rstn`. A config write (`WE`) latches only the coefficient/frequency + registers; nothing in the write path clears the IIR datapath. State + carries across the jump. *(Usage note: `WE` latches all registers + together, so a frequency-only jump must rewrite `C0`/`C1`/`G` with their + existing values to avoid perturbing the linewidth.)* +3. **No cancellation-breaking transient -- the earlier concern was wrong.** + Remod reuses the **identical carrier sample** that demod used: + `dds_dout_la = dds_dout` delayed by exactly `prod0(4) + iir(4) = 8`, which + is the pipeline latency between the two multipliers. So `prod1[t]` + multiplies by `dds_dout[t-8]` and `iir_out[t]` was demodulated by + `conj(dds_dout[t-8])` -- the same sample -- giving + $e^{-j\varphi}e^{+j\varphi}=1$ **sample-for-sample, for any carrier + trajectory including a step**. The `din_la = DDS_LATENCY = 10` tap + likewise aligns each input sample with the frequency commanded when it + entered. There is no spurious pipeline glitch at the jump. + +**Verdict:** at the datapath level the emulator handles a mid-readout jump +*physically correctly* -- the stored field keeps its amplitude and phase, +immediately rings at the new frequency, and decays at the unchanged +$\kappa/2$, with the drive now at the new detuning building the new steady +state. The one real transient is the physical one (old field ringing down +while the new one builds), which is exactly what we want. + +**The genuine v4 gap is the switching *mechanism*, not the datapath.** +Neither existing path cleanly does a *triggered, persistent, +phase-continuous* step between two resonant frequencies: + +- The **chirp/trigger** path (`i_trigger` -> `ramp_gen`) sweeps `pinc` for + `N` steps and **settles back to `FREQ_REG`** -- a sweep-and-return, not a + persistent two-level switch. +- The **register write** of `DDS_FREQ_REG` is persistent but not + time-deterministic (AXI + `WE` CDC latency; and `WE` is a + software-pulsed bit crossing clock domains -- the classic + sim-passes/hardware-flakes hazard). + +For ground<->excited "on demand, triggered from the tProc at a precomputed +time," v4 wants a small **triggered frequency-select** (e.g. two latched +`FREQ` values and a per-lane trigger that toggles which one drives `pinc`), +keeping the accumulator free-running so the switch stays phase-continuous. +This is the one new bit of RTL the jump actually needs. + +### 5.6. Latency reference (from the RTL) + +```{list-table} +:header-rows: 1 +:widths: 40 20 40 + +* - Path + - Cycles + - Source +* - DDS block + - `DDS_LATENCY = 10` + - `dds_phase_ctrl (2) + DDS IP (8)` +* - `din` alignment to carrier + - `din_la = DDS_LATENCY = 10` + - fixes chirp demod alignment +* - Carrier reuse demod->remod + - `dds_dout_la = 8` + - `prod0 (4) + iir (4)` -- guarantees $e^{-j\varphi}e^{+j\varphi}=1$ +* - `kidsim` main path + - `10 + 4 + 4 + 4 = 22` + - DDS + prod0 + iir + prod1 +* - `kidsim_top` total + - `24` + - `punct (2) + KIDSIM_LAT (22)` +``` + +Two independent timing constraints not to conflate (per the `kidsim` +header): `dds_dout_la = 8` guarantees carrier cancellation for *any* input; +`din_la = DDS_LATENCY` selects *which* input sample is demodulated at each +instantaneous chirp frequency and only matters when the input varies in +time. + +### 5.7. Validation plan (what a demonstrating notebook should show) + +A short QICK notebook (or xsim testbench) can turn the theory above into +measured numbers: + +1. **Linewidth / Q.** Sweep the probe across $f_0$, measure the transmitted + magnitude, fit a Lorentzian, and check FWHM $= (1-C_1)f_s/\pi$ and + $Q_L = \pi f_0/((1-C_1)f_s)$ against the table for a few `C1` values. +2. **Ringdown.** Drive on resonance to steady state, switch the drive off, + fit the decay, and check $\tau = 1/((1-C_1)f_s)$. +3. **Notch depth vs coupling.** Set $C_0$ near $C_1$, sweep the gap + $C_0 - C_1$, and confirm the on-resonance depth tracks + $\kappa_i/\kappa = (1-C_0)/(1-C_1)$. +4. **The jump (the important one).** Reach steady state, jump + `DDS_FREQ_REG` (ground->excited), and plot the lab-frame output through + the transition. Confirm: field amplitude/phase continuous, new-frequency + ringing, decay at the unchanged $\kappa/2$. This is also the test that + surfaces condition (1)/(3) of §5.5 if the DDS is not phase-continuous. +5. **Fake T1.** Trigger the ground->excited jump from the tProc at a + precomputed time and show the readout following the programmed decay. + +**Characterization notes (measured in xsim):** + +- The datapath advances **every clock**, not per input beat: the DDS IP + free-runs its output valid, so `dds_valid` (hence `prod0`/IIR/`prod1`) is + asserted every cycle regardless of how sparse `din_valid` is. The + `iir_1p1z` bubble-gating is therefore moot under a running carrier. + Consequence for the scale factors: the decay is $C_1$ **per clock**, so + $\tau = 1/((1-C_1)f_s)$ is per-sample-at-$f_s$ exactly as in §5.3 -- + confirmed by a ringdown showing $|y_1|[n]=|y_1|[0]\,C_1^{n}$ per clock. (A + test that streams sparse beats measures $C_1^{\text{gap}}$ per beat and + looks wrong; drive contiguously.) +- A small **DC floor** sits under the ringdown: the two `cmult` stages use + round-half-up (`ROUND=1`), whose $+\tfrac12$ LSB bias is integrated by the + pole to $\approx \text{bias}/(1-C_1)$ (~=13 LSB at $C_1=0.9$). Harmless for + the emulation, but measure the linewidth well above it. + +### 5.8. Open items feeding v4 + +- Decide whether to keep hand-tuned `C0`/`C1`/`G` or add a + **physics->coefficient** helper (bilinear transform from + $\kappa_i,\kappa_c,\omega_0$). Recommended. +- Decide whether Fano asymmetry matters for the intended use; if so, add a + **complex gain** stage. +- §5.5 conditions (1)-(3) are **resolved in RTL**: phase-continuous DDS, + preserved IIR state, exact carrier cancellation through a step. The + remaining jump work is the **triggered persistent frequency-select** (two + latched `FREQ` values + per-lane trigger, accumulator kept free-running), + and the residual DDS-model eyeball of condition (1). Lock the datapath + behaviour with the jump test of §5.7.4. +- This note documents the **passive resonator**. The **active RF qubit + emulator** (DDS LO + Bloch-state registers + demod-driven state rotation + -> Rabi) is a separate v4 datapath and gets its own note. + +## Related Documentation + +* {doc}`index` and {doc}`avg_buffer` -- the real readout chain this IP + emulates a resonator in front of, for hardware-in-the-loop testing. +* `firmware/fusesoc/cores/ip/axis_kidsim_v3/model/kidsim_resonator_demo.ipynb` + -- Python behavioral model / demo notebook. diff --git a/docs/source/firmware/readouts/mr_buffer_et.rst b/docs/source/firmware/readouts/mr_buffer_et.rst new file mode 100644 index 000000000..07fe2cc15 --- /dev/null +++ b/docs/source/firmware/readouts/mr_buffer_et.rst @@ -0,0 +1,411 @@ +======================================================== +Multi-Rate Buffer (mr_buffer_et) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +The **Multi-Rate buffer with External Trigger** (``mr_buffer_et``) is a +small on-chip capture buffer that taps a readout's *undecimated* data +stream -- the downconverted but not-yet-decimated samples, straight out of +the digital mixer, before the readout's FIR/decimate-by-N stage. This is in +contrast to :doc:`avg_buffer`'s raw buffer and the DDR4 buffer +(``axis_buffer_ddr_v1``), which both see the *decimated* stream. Because it +sits upstream of decimation, ``mr_buffer_et`` sees the full ADC-rate signal +(useful for looking at spurs, noise, or transient/settling behavior that +decimation would filter out), at the cost of a much smaller capture depth +(on-chip BRAM, not DDR4). The module lives in the ``qick`` firmware +repository under ``firmware/ip/mr_buffer_et/`` and is exposed to Python +through ``qick.drivers.readout.MrBufferEt``. + +.. note:: + + The RTL under ``firmware/ip/mr_buffer_et/src/`` (``mr_buffer_et.sv``) is + a SystemVerilog rewrite whose file header identifies it as VLNV + ``qick:ip:mr_buffer_et:1.2.0`` / ``@version 1.2``. However, as of this + writing the packaged ``component.xml`` in the same directory (Xilinx IP + packager metadata) still lists the legacy VHDL sources + (``mr_buffer_v1_0.vhd`` and friends) and reports IP version ``1.1``, and + the Python driver's ``bindto`` list (``qick_lib/qick/drivers/readout.py``) + only recognizes VLNVs ending in ``:1.0`` and ``:1.1``. Board designs + checked in this repository (e.g. + ``firmware/projects/qick_tprocv2_111_standard/bd_2023-1.tcl``) also + instantiate ``QICK:QICK:mr_buffer_et:1.1``. This document describes the + behavior of the SystemVerilog source in ``src/`` (which is what a fresh + IP-XACT repackage would pick up), and calls out anywhere that source + adds behavior (the ``STATUS``/``capture_done`` register, notably) that + the current driver does not yet use. It was **not** verified against the + actual synthesized bitstream of any specific board release -- the two + register fields the driver does use (``dw_capture_reg``, + ``dr_start_reg``) are unchanged between the VHDL and SystemVerilog + implementations, so this is not expected to matter in practice. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +``mr_buffer_et`` (top level ``mr_buffer_et.sv``, wrapping the core +``mr_buffer.sv``) captures ``NM`` parallel channels of ``B`` bits each into +``NM`` independent BRAMs (depth ``2**N`` per BRAM) at full write-side rate +(``s_axis_aclk``). A capture is armed in software (``dw_capture_reg=1``) +and then started by an external hardware ``trigger`` input; once the +window fills, the captured data is drained out as a single ``B``-bit +AXI4-Stream on the read side (``m_axis_aclk``) when software pulses +``dr_start_reg=1``. Per the module's own header comment +(``firmware/ip/mr_buffer_et/src/mr_buffer_et.sv``), the intended flow is: + +1. SW writes ``dw_capture_reg=1`` -- arm the writer. +2. External ``trigger`` fires -- BRAMs fill with ``2**N`` samples/channel. +3. SW writes ``dr_start_reg=1`` -- stream the whole window out. +4. AXI-DMA S2MM drains ``M_AXIS`` to memory (read burst = ``NM*2**N`` + beats, ending in ``tlast``). + +The ``NM`` parallel input channels exist because the source data arrives +faster than one sample per fabric clock: per the QICK demo notebook +(``qick_demos/08_Special_buffers.ipynb``), the MR buffer "is fed by the +downconverted but undecimated data stream coming out of the readout, so +you get 8 samples per fabric tick, or 1 sample per ADC tick." This lines +up with the RTL: ``axis_dyn_readout_v1``'s downsampling ratio is 8 +(``DOWNSAMPLING = 8`` on ``AxisDynReadoutV1`` in +``qick_lib/qick/drivers/readout.py``), and the default synthesis parameter +is ``NM=8`` -- so in typical firmware images, one fabric-clock beat of the +undecimated stream carries all ``NM`` samples that will land at ``ADC +rate`` in real time, and ``mr_buffer_et`` writes all ``NM`` of them into +``NM`` separate BRAMs in that same beat. + +In board designs (see e.g. +``firmware/projects/qick_tprocv2_111_standard/bd_2023-1.tcl``), +``mr_buffer_et`` sits downstream of a many-to-one AXI4-Stream switch +(``axis_switch_mr``) whose inputs are the ``m0_axis`` ports of the board's +dynamic readouts (``axis_dyn_readout_v1``) -- the undecimated tap; compare +with the ``m1_axis`` port of the same readouts, which is the decimated +stream that feeds :doc:`avg_buffer`. Its own ``m00_axis`` output feeds an +AXI-DMA (``axi_dma_mr``) that drains captured data to PS-side (CPU) +memory. In that same board design the instance is parameterized with +``B=32`` (a packed 16-bit I / 16-bit Q sample) and ``N=10`` (1024-deep +per-channel BRAM), leaving ``NM`` at its RTL default of 8 -- giving a +total capacity of ``8*1024 = 8192`` I/Q samples. + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - RTL Default + - Description + * - ``NM`` + - 8 + - Number of parallel memories/channels captured per write-side clock + (``mr_buffer_et.sv``). Each channel is written into its own BRAM. + * - ``N`` + - 8 + - Address width of each channel's BRAM; depth is ``2**N`` samples. + * - ``B`` + - 16 + - Data width, in bits, of one sample in one channel + (``s_axis_tdata`` is ``NM*B`` bits wide overall; ``m_axis_tdata`` is + ``B`` bits wide, one channel's sample per beat on read-back). + * - ``C_S_AXI_DW`` + - 32 + - AXI4-Lite data width (control-register bus). + * - ``C_S_AXI_AW`` + - 6 + - AXI4-Lite address width. With 16 32-bit registers this must be at + least ``$clog2(4) + $clog2(16) = 6`` bits, which is exactly what the + RTL uses. + * - ``DEBUG`` + - 0 + - When 1, ``s_dbg_probe``/``m_dbg_probe`` (32 bits each) are driven out + for an ILA; when 0 they are tied to zero. + +The RTL defaults above (``NM=8``, ``N=8``, ``B=16``) are not what real +boards use -- e.g. ``qick_tprocv2_111_standard`` overrides ``B=32`` and +``N=10`` (see Section 1) while leaving ``NM`` at its default of 8. Total +capture depth is always ``cfg['maxlen'] = NM * 2**N`` samples (this is +also how the Python driver computes it -- see Section 5). + +-------------------------------------------------------------------- +3. Datapath +-------------------------------------------------------------------- + +.. figure:: /images/firmware/mr_buffer_et-blocks.svg + :align: center + :width: 85% + + Top-level block diagram: ``NM`` independent capture/storage lanes + (``axis_to_bram_trig`` + ``bram_tdp``) on the write-side clock, + drained through a single ``bram_to_axis_nt`` reader on the read-side + clock -- matches ``mr_buffer.sv``'s own header description exactly. + +3.1 Write side -- ``axis_to_bram_trig`` (one instance per channel) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``mr_buffer.sv`` slices the wide ``s_axis_tdata`` (``NM*B`` bits) into +``NM`` per-channel ``B``-bit slices and instantiates one +``axis_to_bram_trig`` plus one true-dual-port BRAM (``bram_tdp``) per +channel. Each ``axis_to_bram_trig`` (``firmware/ip/mr_buffer_et/src/axis_to_bram_trig.sv``) +runs a 4-state FSM: + +``INIT_ST -> TRIGGER_ST -> CAPTURE_ST -> END_ST`` + +* **INIT_ST**: idle; waits for ``capture_reg=1`` (the synchronized + ``dw_capture_reg``); counters held cleared. +* **TRIGGER_ST**: armed, waiting for a ``trigger`` pulse (the arm can still + be cancelled here by dropping ``capture_reg``). +* **CAPTURE_ST**: accepts AXI4-Stream input (``s_axis_tready`` asserted) + and moves data from a small internal FIFO into the channel's BRAM. + Retires to ``END_ST`` either when the capture window fills (``2**N`` + samples written) or ``capture_reg`` drops (an abort). +* **END_ST**: stops accepting new stream beats but keeps draining whatever + is still queued in the FIFO into BRAM, so the tail of a capture is never + lost when ``capture_reg`` drops mid-stream. Holds ``o_capture_done=1`` + (a level, not a sticky bit) until software de-asserts ``capture_reg`` and + the internal FIFO is empty, at which point it returns to ``INIT_ST``. + +An internal 16-deep synchronous FIFO (``fifo_sync``, first-word-fall-through) +decouples the AXI4-Stream handshake from the BRAM write pipeline; the +module's own header comments state the write-side invariants explicitly: +every beat actually accepted (``tvalid && tready``, not ``tvalid`` alone) +is written to the FIFO exactly once, every word popped from the FIFO while +the window still has room is committed to BRAM (the write pipeline is +driven off ``fifo_rd_en`` itself, so a state transition can't cancel an +in-flight write), and once the window is full further FIFO words are still +popped (and discarded) rather than stalling, so the FSM can still reach +``fifo_empty`` and retire. ``s_axis_tready`` from all ``NM`` channels is +AND-reduced (``mr_buffer.sv``) into the block's single ``s_axis_tready`` +output -- all channels must have room, since they're written from the same +wide input word. + +``o_capture_done`` from each of the ``NM`` per-channel FSMs is AND-reduced +in ``mr_buffer.sv`` into ``o_capture_done``, which crosses into +``s_axi_aclk`` through a 2-stage ``cdc_bit_sync`` and lands in the +AXI4-Lite ``STATUS`` register (register index 2, bit 0 -- see Section 4). + +3.2 Read side -- ``bram_to_axis_nt`` (shared, reads all channels) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +On the read side, all ``NM`` BRAMs' port-B outputs are concatenated into +one wide bus and fed to a single ``bram_to_axis_nt`` instance +(``firmware/ip/mr_buffer_et/src/bram_to_axis_nt.sv``), which walks every +address (0 to ``2**N-1``) and every channel (0 to ``NM-1``) at each +address, serializing the whole window onto the ``B``-bit +``m_axis_tdata``/``m_axis_tvalid``/``m_axis_tlast`` output. Output word +order for one read burst (``TOTAL_WORDS = NM*2**N``): + +:: + + word 0 : ch0 @ addr 0 + word 1 : ch1 @ addr 0 + ... + word NM-1 : ch(NM-1) @ addr 0 + word NM : ch0 @ addr 1 + ... + word NM*2**N - 1 : ch(NM-1) @ addr (2**N - 1) <- tlast here + +FSM: ``INIT_ST -> READ_ST -> WRITE_ST -> ... -> READ_LAST_ST -> +WRITE_LAST_ST -> FIFO_ST -> END_ST -> INIT_ST``, started by ``start_reg`` +(the synchronized ``dr_start_reg``). ``READ_ST``/``WRITE_ST`` alternate, +reading one BRAM address then cycling the channel selector through all +``NM`` channels at that address before advancing to the next address; +``READ_LAST_ST``/``WRITE_LAST_ST`` handle the final address, tagging the +very last channel's word with ``tlast``. Output beats are queued through +another internal FIFO (16 deep, widened by one bit to carry the ``tlast`` +tag alongside the data, so ``tlast`` can never drift out of step with its +beat). + +The module's own header documents a real bug fixed between v1.0 and v1.1 +of this file that's worth knowing about if you ever touch this RTL: v1.0 +retired ``FIFO_ST`` on the FIFO's ``empty`` flag and gated +``m_axis_tvalid`` on the FSM state, but Xilinx's XPM ``empty`` flag +deasserts a cycle or two *after* a write, so the FSM could sample +``empty=1`` while the final words (including the ``tlast`` beat) were +still in flight, strand them, and hang a downstream AXI-DMA waiting for a +``tlast`` that never came. v1.1 fixes this with an exact flight counter +(``fifo_level``, incremented/decremented directly from the write/read +strobes rather than from the XPM status flags) that ``FIFO_ST`` uses to +decide when the burst has truly drained, and by only gating +``m_axis_tvalid`` off in ``INIT_ST`` (never mid-burst). + +3.3 Clock-domain crossings +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Three control signals cross clock domains via ``cdc_gray_sync`` (a 2-stage, +per-bit synchronizer -- despite the name, it does not gray-decode; see +``cdc_gray_sync.sv``, used here with ``N=2``, ``M=1``): + +* ``dw_capture_reg`` (AXI-Lite domain) -> ``s_axis_aclk``. +* ``trigger`` OR'd with the debug force-trigger bit (``debug_reg[0]``) -> + ``s_axis_aclk``. +* ``dr_start_reg`` (AXI-Lite domain) -> ``m_axis_aclk``. + +and ``o_capture_done`` (from ``s_axis_aclk``, the write-side FSMs) crosses +back into ``s_axi_aclk`` via a 2-stage ``cdc_bit_sync`` in +``mr_buffer_et.sv`` before landing in the ``STATUS`` register. + +3.4 Debug forcing +^^^^^^^^^^^^^^^^^ + +The 32-bit ``debug_reg`` (register 15, RW) has two functional bits +(``mr_buffer.sv``): bit 0 ORs a software-forced pulse into the hardware +``trigger`` input before CDC, and bit 1 forces ``s_axis_tready`` high +regardless of the AND-reduced per-channel ready signals +(``s_axis_tready = (&s_axis_tready_i) | s_axis_tready_force``) -- useful +for driving the input stream during bring-up without a real trigger +source or without every channel individually asserting ready. + +-------------------------------------------------------------------- +4. Register Map +-------------------------------------------------------------------- + +The AXI4-Lite interface is a generic 16-register, 32-bit-per-register +slave (``axil_slv.sv``, ``NUM_REGS=16``, ``AXI_DW=32``); with +``C_S_AXI_AW=6`` the register index is address bits ``[5:2]`` (byte +address = register index * 4). Only register 2 is marked read-only in +hardware (``RO_MASK = 16'b0000_0000_0000_0100``, i.e. bit 2); all other +registers are plain read/write and reset to 0 (``mr_buffer_et.sv`` does +not override ``axil_slv``'s default zero ``REG_INIT_VAL``). + +.. list-table:: + :header-rows: 1 + :widths: 15 20 15 50 + + * - Index + - Register + - Width + - Description + * - 0 + - ``dw_capture_reg`` + - 1 bit (RW) + - 1 = arm the writer (capture-enable). Must be de-asserted and + re-asserted to allow a new capture (per the driver's own + docstring comment in ``qick_lib/qick/drivers/readout.py``). + * - 1 + - ``dr_start_reg`` + - 1 bit (RW) + - 1 = start the reader (drain the captured window out on + ``m_axis``). Must be de-asserted and re-asserted to allow a new + transfer. + * - 2 + - ``STATUS`` (``capture_done``) + - 1 bit (RO) + - Bit 0 mirrors ``o_capture_done`` (synchronized into + ``s_axi_aclk``): high once the write-side FSMs have all reached + ``END_ST``. **Not currently read by the Python driver** -- see the + note in Section 5. + * - 3-14 + - reserved + - -- + - Unused; reads as 0. + * - 15 + - ``debug_reg`` + - 32 bits (RW) + - Bit 0: force ``trigger``. Bit 1: force ``s_axis_tready`` high (see + Section 3.4). Remaining bits unused. + +-------------------------------------------------------------------- +5. Python Interface +-------------------------------------------------------------------- + +The block is exposed through ``qick.drivers.readout.MrBufferEt`` +(``bindto`` covers VLNVs ``user.org:user:mr_buffer_et:1.0``/``1.1`` and +``QICK:QICK:mr_buffer_et:1.0``/``1.1`` -- see the version note at the top +of this page). ``QickSoc`` auto-discovers at most one ``MrBufferEt`` +instance that is actually wired to a readout (multiple would raise +``RuntimeError``, see ``qick_lib/qick/qick.py``) and exposes it as +``soc.mr_buf``, plus two convenience methods on ``QickSoc`` itself: + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + # Arm the buffer for readout channel RO_CH (selects the right switch + # input, then toggles capture off/on so a fresh capture always starts + # from a clean state). + soc.arm_mr(ch=RO_CH) + + # Trigger it from a program -- add mr=True to any trigger() call, + # or call it once in initialize() since later triggers are ignored + # until the buffer is re-armed. + prog = QickProgram(soccfg) + prog.declare_readout(ch=RO_CH, freq=FREQ, length=100, gen_ch=GEN_CH) + prog.trigger(mr=True) + prog.end() + prog.config_all(soc) + soc.tproc.start() + + # Read back the captured window: an (n_samples, 2) array of int16, [:,0]=I, [:,1]=Q + iq = soc.get_mr() + +``soc.arm_mr(ch)`` calls ``mr_buf.set_switch()`` (routes the shared +``axis_switch_mr`` to the given readout channel -- or, if the board has no +switch, just asserts that the requested channel is the one hardwired in) +followed by ``mr_buf.disable()``/``mr_buf.enable()`` (toggle +``dw_capture_reg`` 0->1 to arm a fresh capture). ``soc.get_mr(start=None)`` +calls ``mr_buf.transfer(start)``, which sets ``dr_start_reg=1``, DMAs the +whole ``NM*2**N``-sample window into a preallocated buffer +(``self.buff``, a ``pynq.allocate``'d ``np.int16`` array of shape +``2*cfg['maxlen']``) via the traced-out AXI-DMA (``self.dma``), clears +``dr_start_reg``, and returns ``np.copy(self.buff).reshape((-1,2))[start:]``. + +``cfg['maxlen']`` is computed as ``2**N * NM`` from the synthesis +generics read out of the block description (``_init_config``), matching +the ``TOTAL_WORDS`` quantity from Section 3.2. + +.. note:: + + The driver's ``REGISTERS`` dict only defines ``dw_capture_reg`` (index + 0) and ``dr_start_reg`` (index 1) -- it does not read the ``STATUS`` + register (index 2) described in Section 4, so ``capture_done`` is not + currently surfaced to Python; ``transfer()`` relies on the AXI-DMA + S2MM transfer completing (``self.dma.recvchannel.wait()``) rather than + polling hardware capture-done status. + +.. note:: + + ``_init_config`` sets ``cfg['junk_len'] = 8`` if the bound IP type + string contains ``mr_buffer_et:1.0``, else ``0`` -- a workaround for a + firmware bug (fixed in IP version 1.1, per git history) where the first + several samples of a transfer read back as stale/junk data. + ``transfer(start=None)`` defaults ``start`` to this ``junk_len`` and + skips that many samples from the front of the returned array. Note + that the demo notebook (``qick_demos/08_Special_buffers.ipynb``) + describes this as "the first 8 samples are always stale" without the + version conditional -- treat the driver's version-gated + ``cfg['junk_len']`` as the authoritative value for a given board's + bound IP version. + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`avg_buffer` -- the buffer that captures the *decimated* stream + from the same readouts (accumulated and raw-decimated modes); + ``mr_buffer_et`` instead captures the undecimated stream. +* :doc:`readout_v2` and :doc:`readout_dynamic` -- the readouts whose + undecimated output (``m0_axis`` on the dynamic readouts) feeds this + block via ``axis_switch_mr``. +* :doc:`/tprocv2_trm` -- the tProcessor trigger port that starts a + capture, via ``prog.trigger(mr=True)``. + +.. note:: + + ``AxisBufferDdrV1`` (``qick.drivers.readout.AxisBufferDdrV1``, RTL + under ``firmware/ip/axis_buffer_ddr_v1/``) is the other DDR4-adjacent + buffer block and is easy to confuse with ``mr_buffer_et``, but the two + are quite different. Per its own driver docstring, ``AxisBufferDdrV1`` + "is fed by the downconverted+decimated data stream coming out of the + readout" -- i.e. the *same* stream that feeds :doc:`avg_buffer`, not + the undecimated stream ``mr_buffer_et`` sees -- and it streams + continuously into off-chip DDR4 memory (via an AXI memory-mapped port, + ``rstart_reg``/``wstart_reg``/``wnburst_reg`` etc.) rather than a small + on-chip BRAM, giving it a vastly larger (multi-GiB) but still + finite/wraparound capture depth. Its Python surface is + ``soc.arm_ddr4()``/``soc.get_ddr4()`` rather than + ``soc.arm_mr()``/``soc.get_mr()``. diff --git a/docs/source/firmware/readouts/readout_dynamic.rst b/docs/source/firmware/readouts/readout_dynamic.rst new file mode 100644 index 000000000..9b9898b63 --- /dev/null +++ b/docs/source/firmware/readouts/readout_dynamic.rst @@ -0,0 +1,511 @@ +======================================================================== +Dynamic Readouts (axis_readout_v3 / axis_dyn_readout_v1) - QICK Firmware +======================================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_readout_v3** and **axis_dyn_readout_v1** are the *dynamic* counterparts +of :doc:`readout_v2`: digital down-converter (DDC) IP blocks that sit +between the RFDC's ADC output and :doc:`avg_buffer`, 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 :meth:`.QickProgram.declare_readout` calls a +readout "controlled by tProc" (see :ref:`readout-dyn-python` below) -- +config­uration 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), and + ``m1_axis``, the final decimated stream (1 complex sample/clock) that feeds + :doc:`avg_buffer`. + +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. + +.. figure:: /images/firmware/dyn_readout-blocks.svg + :align: center + :width: 80% + + 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 :doc:`avg_buffer`. ``fifo_sync`` carries the tProc-pushed + config descriptor (freq/phase/mode) into ``down_conversion_dynro``. + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: axis_readout_v3 vs. axis_dyn_readout_v1 -- key parameters + :header-rows: 1 + :widths: 30 20 20 30 + + * - Parameter + - axis_readout_v3 + - axis_dyn_readout_v1 + - Notes + * - Input samples/clock (``N``/``N_DDS``) + - 4 + - 8 + - Parallel real ADC lanes into the down-conversion multiplier; also the + number of DDS instances (``dds_0``/``dds_compiler_0``). + * - FIR decimation ratio + - 4 + - 8 + - Xilinx FIR Compiler 7.2, ``Filter_Type=Decimation``. Exposed to Python + as ``DOWNSAMPLING`` on the driver class (``4`` / ``8``). + * - FIR coefficients + - 107 taps (``fir_0/fir.coe``) + - 120 taps (``fir.coe``) + - Symmetric low-pass, fixed/compiled into the bitstream (not + runtime-loadable). + * - FIR coefficient/data width + - 16 bits + - 16 bits + - ``Coefficient_Width``/``Data_Width`` on both FIR IPs. + * - FIR output width (per I/Q component) + - 18 bits + - 16 bits + - See :ref:`readout-dyn-rounding` 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_0``/``dds_compiler_0``, ``Phase_Width=32``, + ``Output_Width=16``). + * - DDS latency + - 10 cycles + - 8 cycles + - IP-configured pipeline latency of the DDS compiler core. + * - Config-descriptor FIFO depth + - 8 entries + - 8 entries + - ``fifo #(.B(88), .N(8))`` in both top levels -- up to 8 pending + ``s0_axis`` configuration words can be queued. + * - Config descriptor width + - 88 bits + - 88 bits + - See :ref:`readout-dyn-descriptor`. + +-------------------------------------------------------------------- +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: + +1. **down_conversion** (``down_conversion.sv``) -- reads one descriptor at a + time out of the FIFO via an internal ``ctrl`` FSM (see + :ref:`readout-dyn-fsm`), drives 4 parallel ``dds_0`` DDS 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. +2. **down_conversion_fir** (``down_conversion_fir.sv``) -- feeds the 4 + parallel 32-bit (16+16 I/Q) product-domain samples/clock into ``fir_0`` + (decimation by 4), producing one complex sample/clock on ``m_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: + +1. **down_conversion** (``down_conversion.v``) -- same structure as v3's + (shared ``ctrl_dyn_ro_v1`` FSM, per-lane DDS + complex multiply), but with + ``N_DDS=8`` lanes 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 parametrized ``latency_reg`` block as + in v3). This stage's output is exposed directly as ``m0_axis`` (8 complex + samples/clock, pre-filter/pre-decimation, still passed through the + output-selection mux). +2. **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 consumes ``m0_axis`` directly and produces the final + 1-complex-sample/clock stream on ``m1_axis``. + +.. _readout-dyn-fsm: + +3.3 Shared control FSM (``ctrl.sv`` / ``ctrl_dyn_ro_v1.sv``) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Both blocks' control logic is built from what is essentially the same +two-state FSM (``READ_ST``/``CNT_ST``, one-hot encoded): + +* In ``READ_ST``, the FSM asserts ``fifo_rd_en`` and, once a descriptor is + available (``~fifo_empty``) or the previously-loaded descriptor's + ``mode`` bit is 1 ("periodic"), latches it and moves to ``CNT_ST``. +* In ``CNT_ST`` it holds that descriptor's frequency/phase/output-select + settings for ``nsamp-2`` clock cycles (counted by a free-running counter, + ``cnt``), then returns to ``READ_ST`` to look for (or wait for) the next + descriptor. + +From the latched ``freq``/``phase`` fields the FSM computes, per DDS lane +``i``, a phase-coherent phase word ``phase_v1[i] = i*pinc + running_phase``, +where the running phase accumulates ``pinc*N`` per clock (``N`` = 4 or 8, the +number of lanes) and resets to the descriptor's ``phase`` field whenever +``sync`` (a new descriptor being loaded) coincides with the descriptor's +``phrst`` bit being set. This is what makes the DDS phase-coherent across +consecutive tProc-issued descriptors except where the program explicitly +requests a phase reset. + +**One difference between the two FSMs**: ``ctrl.sv`` (axis_readout_v3) also +computes an output-enable signal ``en`` (asserted once a descriptor has been +loaded and the FIFO isn't empty, deasserted in periodic mode's gaps). However, +tracing it through ``down_conversion.sv`` shows this signal is latency-matched +(``en_latency_reg_i``) but **never wired to anything that gates the +datapath** -- ``down_conversion.sv`` computes its outputs unconditionally +(the source comment there reads *"Always on output (not using en like in +signal_gen_v6)"*), and ``m_axis_tvalid`` is hardwired to 1. So in the current +axis_readout_v3 RTL, ``en`` is a vestigial signal with no observable effect. +Consistent with that, ``ctrl_dyn_ro_v1.sv`` (used by axis_dyn_readout_v1) has +the entire ``en``/output-enable logic commented out and does not produce an +``en`` port at all -- one confirmed, RTL-verified difference between the two +control modules, not just a naming variant. + +.. _readout-dyn-descriptor: + +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: + +.. list-table:: Configuration descriptor (``s0_axis_tdata``, 88 bits) + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Bits + - Description + * - (reserved) + - ``[87:84]`` + - Unused ("xxxx" in the source comment). + * - ``phrst`` + - ``[83]`` + - 1: reset the phase-coherent DDS accumulator when this descriptor is + loaded. 0: keep accumulating phase from the previous descriptor. + * - ``mode`` + - ``[82]`` + - 0: hold this descriptor's settings for ``nsamp`` cycles, then load the + next one. 1: periodic -- keep re-loading (repeating) this descriptor + without waiting for a new one to arrive on ``s0_axis``. + * - ``outsel`` + - ``[81:80]`` + - Output source select (see :ref:`readout-dyn-outsel`). + * - ``nsamp`` + - ``[79:64]`` + - Number of clock cycles (not decimated output samples) this + descriptor's settings are held for, in one-shot mode. + * - ``phase`` + - ``[63:32]`` + - DDS phase offset. + * - ``freq`` + - ``[31:0]`` + - 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 +:doc:`readout_v2`'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. + +.. _readout-dyn-outsel: + +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: + +.. list-table:: + :header-rows: 1 + :widths: 15 25 60 + + * - ``outsel`` + - 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 :meth:`.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. + +.. _readout-dyn-rounding: + +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 :meth:`.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 is + ``Symmetric_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 sets ``IQ_OFFSET = -0.5`` for + ``AxisReadoutV3``. +* **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_0P5`` where + ``RND_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_o`` is wired straight from + ``fir_compiler_0``'s output). This explicit rounding is consistent with + the Python driver's ``IQ_OFFSET = 0.0`` for ``AxisDynReadoutV1``. + +-------------------------------------------------------------------- +4. Why There's No Register Map +-------------------------------------------------------------------- + +Unlike ``axis_readout_v2`` (:doc:`readout_v2`), 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: + +.. list-table:: axis_readout_v3 top-level ports + :header-rows: 1 + :widths: 20 15 15 50 + + * - Port + - Direction + - Width + - Description + * - ``s0_axis`` + - slave + - 88 bits + - Configuration descriptors, pushed by the tProcessor (see + :ref:`readout-dyn-descriptor`). Backpressured by the 8-entry FIFO + (``s0_axis_tready = ~fifo_full``). + * - ``s1_axis`` + - slave + - 4x16 bits + - ADC input, 4 real samples/clock. + * - ``m_axis`` + - master + - 32 bits + - Decimated complex (I, Q) output, 1 sample/clock, to + :doc:`avg_buffer`. + +.. list-table:: axis_dyn_readout_v1 top-level ports + :header-rows: 1 + :widths: 20 15 15 50 + + * - Port + - Direction + - Width + - Description + * - ``s0_axis`` + - slave + - 88 bits + - Configuration descriptors, pushed by the tProcessor. + * - ``s1_axis`` + - slave + - 8x16 bits + - ADC input, 8 real samples/clock. + * - ``m0_axis`` + - master + - 8x32 bits + - Down-converted output, before FIR/decimation, 8 complex samples/clock. + * - ``m1_axis`` + - master + - 32 bits + - Decimated complex (I, Q) output, 1 sample/clock, to + :doc:`avg_buffer`. + +Because there is no register interface, there is also no PYNQ overlay driver +in the usual sense -- see :ref:`readout-dyn-python`. + +.. _readout-dyn-python: + +-------------------------------------------------------------------- +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 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:meth:`.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): + +.. code-block:: python + + 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: + +* :meth:`.QickProgramV2.add_readoutconfig` (``ch``, ``name``, ``freq``, + ``phase``, ``outsel``, ``phrst``, ...) computes the register values (DDS + frequency/phase words, and a packed ``outsel``/``mode``/``phrst`` + configuration word via ``cfg2reg``) and stores them as a named "pulse" in + the program. +* :meth:`.QickProgramV2.send_readoutconfig` (``ch``, ``name``, ``t``) emits + the instructions (``ConfigReadout``) that push that pre-computed word to + the readout's ``tproc_ctrl`` port at time ``t``, using the same + ``WPORT_WR`` wave-memory-write mechanism used to schedule signal-generator + pulses (see :doc:`../generators/sg_v6`'s tProc-sequencing section). This is the + run-time source of the ``s0_axis`` descriptor word described in + :ref:`readout-dyn-descriptor`. + +.. code-block:: python + :caption: Configuring a dynamic readout's frequency from a tProc v2 program + + 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. + +Related Documentation +---------------------- + +* :doc:`index` -- readout system overview. +* :doc:`readout_v2` -- the static, PYNQ-register-configured readout these + blocks are the dynamic counterpart of. +* :doc:`avg_buffer` -- the buffer downstream of ``m_axis``/``m1_axis``. +* :doc:`/tprocv2_trm` -- tProcessor instruction reference, including + ``WPORT_WR`` and the timing model used by ``send_readoutconfig()``. +* :doc:`../generators/sg_v6` -- the signal-generator counterpart of tProc-scheduled, + ``WPORT_WR``-driven configuration. +* :doc:`/topics/freq_matching` -- keeping generator and readout frequencies in + sync. diff --git a/docs/source/firmware/readouts/readout_pfb.rst b/docs/source/firmware/readouts/readout_pfb.rst new file mode 100644 index 000000000..6df89d79e --- /dev/null +++ b/docs/source/firmware/readouts/readout_pfb.rst @@ -0,0 +1,474 @@ +=============================================================================== +PFB Readouts (axis_pfb_readout_v2 / v3 / v4) - QICK Firmware +=============================================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_pfb_readout_v2**, **axis_pfb_readout_v3**, and **axis_pfb_readout_v4** +are QICK's polyphase-filter-bank (PFB) readouts: unlike every other readout +documented so far (:doc:`readout_v2`, :doc:`readout_dynamic`), a PFB readout +channelizes its ADC input into many fixed-frequency slices of the band *in +hardware*, then lets software/tProc pick which slice(s) to route to its +output port(s) and fine-tune each with its own DDS. One PFB readout instance +therefore behaves like several single-tone readouts operating simultaneously +on disjoint (or shared) parts of the spectrum, rather than one tunable +downconverter. All three modules live under ``firmware/ip/`` -- +``axis_pfb_readout_v2/``, ``axis_pfb_readout_v3/``, ``axis_pfb_readout_v4/`` +-- and are exposed to Python through ``qick.drivers.readout.AbsPFBReadout`` +and its ``AxisPFBReadoutV2``/``V3``/``V4`` subclasses. + +.. note:: + Like most of the cores in this doc set, this is **legacy RTL that has not + been ported to the newer fusesoc build system** -- it exists only in the + ``firmware/ip/`` tree, not under a fusesoc core description. That does not + make it dead code: it is real, currently-shipping firmware. The flagship + ZCU216 "tpv2_std" board design (``firmware/Top/216/tpv2_std/bd_2023-1.tcl``, + tracked on branch ``feature/114-integrate-hog-fusesoc``, since it predates + this branch's docs-only history) instantiates one **axis_pfb_readout_v4** + alongside two ``axis_dyn_readout_v1`` (:doc:`readout_dynamic`) -- see + :ref:`readout-pfb-zcu216` below. + +.. _readout-pfb-general: + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +All three versions share the same overall pipeline: + +1. **Polyphase filter bank + FFT** -- a bank of parallel FIR filters + (``firs.sv``/``firs``) followed by an FFT splits the input band into N + equally-spaced, overlapping channels, each centered on a different fixed + frequency (an integer multiple of ``fs/N``, where ``fs`` is the ADC sample + rate seen by the PFB). This stage is a fixed, compiled-in channelizer -- + its channel count and spacing are synthesis-time constants, not runtime + registers. +2. **Per-channel or per-output DDS mixing** -- a second, small, *programmable* + DDS fine-tunes each selected channel by up to +/- half a channel width, so + the final demodulation frequency is (fixed PFB channel center) + (DDS + offset), rather than only landing on one of the N coarse PFB bins. +3. **Output routing/selection** -- a subset of the N PFB channels (up to + NOUT of them) is routed to the block's NOUT physical AXI-Stream output + ports, which is what actually gets built (see :ref:`readout-pfb-zcu216`) + -- each output port typically feeds its own :doc:`avg_buffer` instance + downstream, one PFB readout thus driving multiple avg_buffers. + +Where the three versions differ is **N** (PFB channel count), **NOUT** +(selectable/routed output count), and **whether the fine-tuning DDS sits +before or after output selection** -- all verified directly from RTL below, +not inferred from naming. + +.. list-table:: axis_pfb_readout_v2 / v3 / v4 -- top-level comparison + :header-rows: 1 + :widths: 22 20 20 38 + + * - + - v2 + - v3 + - v4 + * - PFB channels (N) + - 8 (``ssrfft_8x8``, ``NFFT=2*L`` with ``L=4``) + - 64 (``pfb_top``/``pfb_readout`` default parameter ``N=64``, + ``ssrfft_8x64``) + - 64 (same ``pfb_readout``/``pfb`` core as v3, unchanged) + * - Selectable outputs (NOUT) + - 4 (``m0``-``m3``) + - 4 (``m0``-``m3``, ``localparam NOUT = 4`` in ``pfb_top.sv``) + - 8 (``m0``-``m7``, ``localparam NOUT = 8`` in ``pfb_top.sv``) + * - DDS placement + - Per PFB channel (all 8 channels always have a live DDS mixer; + ``DDS_ON_OUTPUT = False`` in the driver) + - Per output (4 independent, phase-coherent DDS blocks, one per + selected output; ``DDS_ON_OUTPUT = True``) + - Per output (8 independent DDS blocks, same structure as v3, just + doubled) + * - Output-channel selection + - ``pfb_mux.sv``: a 3-bit ``CH[0-3]SEL_REG`` per output picks 1 of the 8 + already-DDS-mixed channels + - ``pfb_chsel.sv`` (one instance per output, inside ``pfb_top``): a + 16-bit ``ID[0-3]_REG`` (packet + index into the 64-channel TDM + stream) picks which raw PFB channel feeds that output's DDS + - Same ``pfb_chsel.sv`` mechanism as v3, with 8 instances (``ID[0-7]_REG``) + * - AXI-Lite address width + - 6 bits (``s_axi_awaddr[5:0]``, 13 registers) + - 6 bits (``s_axi_awaddr[5:0]``, 12 registers) + - 8 bits (``s_axi_awaddr[7:0]``, 24 registers -- widened because NOUT + doubled) + +-------------------------------------------------------------------- +2. axis_pfb_readout_v2 +-------------------------------------------------------------------- + +2.1 Datapath +^^^^^^^^^^^^ + +``axis_pfb_readout_v2.v`` instantiates a single block, ``pfb_dds_mux`` +(``pfb_dds_mux.sv``), which chains three sub-blocks: + +1. **pfb** (``pfb.sv``) -- ``firs`` (the polyphase FIR bank, ``L=4`` input + lanes) feeds an ``ssrfft_8x8`` core (``NFFT=SSR=2*L=8``, ``B=16``), which + is then run through ``pimod`` (a "pi modulation" / spectral-reordering + stage). Output is 8 interleaved 32-bit (16-bit I + 16-bit Q) lanes on one + 256-bit-wide AXI-Stream word (``tdata_pfb``) -- **8 fixed PFB channels**, + each centered on ``i * fs/16`` for channel ``i`` (the ``ssrfft_8x8`` + parameterization: 8-point FFT over the PFB's overlapped, 50%-overlap + 50%-decimated stream). +2. **ddsprod_v** (``ddsprod_v.sv``) -- instantiates 8 ``ddsprod`` blocks + (``ddsprod.sv``), one per PFB channel, **always active regardless of + which outputs are actually used**. Each ``ddsprod`` complex-multiplies its + channel's PFB output against a per-channel programmable DDS + (``dds_0``/``FREQ[0-7]_REG``), and its output mux (shared 2-bit + ``OUTSEL_REG`` across all 8 channels) picks between the demodulated + product, the raw (delay-matched) PFB channel input, or the raw DDS tone -- + the same product/input/dds encoding pattern used by the single-tone + readouts (:doc:`readout_v2`). +3. **pfb_mux** (``pfb_mux.sv``) -- a pure combinational 8-to-1 mux, replicated + 4 times (one per output). Each output's 3-bit ``CH[0-3]SEL_REG`` selects + which of the 8 already-DDS-mixed channels appears on that output + (``m0``-``m3``). + +Because the DDS sits *before* the output mux (per-channel, not per-output), +v2's 4 outputs can only ever carry (a subset of) the 8 channel-DDS results +computed once, shared if two outputs pick the same channel -- there is no way +to independently re-tune the same PFB channel differently on two outputs +simultaneously. + +.. figure:: /images/firmware/readout_pfb_v2-blocks.svg + :align: center + :width: 85% + + Top-level block diagram: DDS mixing happens once per PFB channel + (``ddsprod_v``, always active for all 8), and ``pfb_mux`` only picks + which already-mixed channel reaches each output -- the "DDS-before-mux" + ordering described above. + +2.2 Register Map +^^^^^^^^^^^^^^^^^ + +Register offsets below are as wired in ``axi_slv.vhd`` (``slv_reg0`` +.. ``slv_reg12``) and match ``AxisPFBReadoutV2._init_config()``'s +``REGISTERS`` dict in ``qick_lib/qick/drivers/readout.py`` exactly. + +.. list-table:: + :header-rows: 1 + :widths: 22 12 66 + + * - Register + - Width + - Description + * - ``FREQ[0-7]_REG`` + - 32 bits each + - Per-PFB-channel DDS tuning word (one per of the 8 fixed channels, + *not* per output). + * - ``OUTSEL_REG`` + - 2 bits + - Shared by all 8 channel DDS muxes. 0: product (demodulated). 1: input + (bypass, raw PFB channel). 2: dds (raw local-oscillator tone). + * - ``CH0SEL_REG`` .. ``CH3SEL_REG`` + - 3 bits each + - Maps output port N (0-3) to one of the 8 DDS-mixed PFB channels + (0-7). + +-------------------------------------------------------------------- +3. axis_pfb_readout_v3 +-------------------------------------------------------------------- + +3.1 Datapath +^^^^^^^^^^^^ + +``axis_pfb_readout_v3.v`` instantiates ``pfb_readout`` (``pfb_readout.v``, +parameterized ``N=64`` channels, ``L=4`` input lanes), which chains: + +1. **pfb_top** (``pfb_top.sv``) -- instantiates the shared ``pfb`` channelizer + core (50%-overlap PFB into an ``ssrfft_8x64``-based 64-point FFT -- + channel ``i`` centered at ``i * fs/64``), whose 64-channel output stream is + time-division-multiplexed across ``L=8`` parallel lanes (``2*L`` from the + overlap structure), i.e. 8 channels appear per clock, taking 64/8 = 8 + clocks ("packets") to cycle through all 64 channels. ``pfb_top`` + instantiates **NOUT = 4** ``pfb_chsel`` blocks in a ``generate`` loop, one + per output -- each is a TDM channel-extractor: it watches the 8-lane + stream for its configured ``(packet, index)`` pair (from that output's + ``ID[0-3]_REG``) and latches the matching 32-bit (I,Q) sample onto its + output. Unlike v2, **no DDS mixing happens at this stage** -- ``pfb_top`` + only selects a raw PFB channel per output; it has no ``FREQ``/``PINC`` + registers of its own. +2. **ddsprod_v** (a *different*, 4-output-specific ``ddsprod_v.sv`` from + v2's 8-channel one) -- instantiates 4 independent ``ddsprod`` blocks, one + per output, **after** channel selection. Each output gets its own + phase-coherent DDS (``PINC[0-3]_REG``/``POFF[0-3]_REG``) applied to + whichever raw PFB channel that output's ``pfb_chsel`` picked. Because the + DDS is per-output rather than per-channel, the *same* PFB channel can be + routed to two different outputs and independently fine-tuned/phased on + each -- the opposite tradeoff from v2. + +v3 has **no ``OUTSEL_REG``**/output-mode-selection mux at all (confirmed: +``ddsprod.sv`` in v3/v4 has no ``OUTSEL_REG`` port, unlike v2's; the Python +driver sets ``HAS_OUTSEL = False`` for ``AxisPFBReadoutV3``) -- each output +always carries the demodulated product, nothing else. + +.. figure:: /images/firmware/readout_pfb_v3-blocks.svg + :align: center + :width: 85% + + Top-level block diagram -- the opposite ordering from v2: + ``pfb_chsel`` picks a raw channel per output first, then ``ddsprod_v`` + applies an independent per-output DDS. v4 (below) is the same diagram + with ``NOUT=8`` instead of 4 -- RTL-identical apart from the doubled + instance arrays. + +3.2 Register Map +^^^^^^^^^^^^^^^^^ + +Confirmed against ``axi_slv.vhd`` (``slv_reg0``..``slv_reg11``, 12 registers, +6-bit address bus) and ``AxisPFBReadoutV3._init_config()``, which builds the +same offsets programmatically (``id%d_reg`` at 0-3, then ``freq%d_reg``/ +``phase%d_reg`` pairs at 4/5, 6/7, 8/9, 10/11). Note the RTL register names +(``ID``/``PINC``/``POFF``) differ from the Python driver's friendlier names +(``id``/``freq``/``phase``) for the same bits -- ``PINC`` = phase increment +(frequency tuning word), ``POFF`` = phase offset. + +.. list-table:: + :header-rows: 1 + :widths: 22 12 66 + + * - Register (RTL / driver name) + - Width + - Description + * - ``ID[0-3]_REG`` / ``id[0-3]_reg`` + - 16 bits + - Selects which of the 64 PFB channels feeds output N. Lower 8 bits = + "packet" (``pfb_ch // 8``, 0-7), upper 8 bits = "index" within that + packet's 8-lane word (``pfb_ch % 8``, 0-7) -- computed by + ``AxisPFBReadoutV3.set_freq_int()`` as + ``id_val = (index << 8) + packet``. + * - ``PINC[0-3]_REG`` / ``freq[0-3]_reg`` + - 32 bits + - Per-output DDS frequency (fine-tuning on top of the selected PFB + channel's fixed center frequency). + * - ``POFF[0-3]_REG`` / ``phase[0-3]_reg`` + - 32 bits + - Per-output DDS phase offset. + +-------------------------------------------------------------------- +4. axis_pfb_readout_v4 +-------------------------------------------------------------------- + +4.1 Is v4 RTL-identical to v3? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**No.** This was verified directly by diffing the RTL trees, not assumed from +the Python driver. ``axis_pfb_readout_v4`` reuses the *same* shared +sub-modules as v3 (``pfb_top.sv``, ``pfb_chsel.sv``, ``pfb.sv``, +``pfb_switch.v``, ``pfb_reorder.sv``, ``pfb_ctrl.vhd``, ``pfb_cfg.vhd`` are +byte-for-byte identical between the two IP directories -- confirmed with +``diff``), but the **top-level wrapper and its NOUT-sized instance arrays are +doubled from 4 to 8**: + +* ``pfb_top.sv``'s own header comment changes from *"The number of outputs is + fixed to 4 in this case"* (v3) to *"...fixed to 8 in this case"* (v4), and + its ``localparam NOUT`` literally changes from ``4`` to ``8``, adding 4 more + ``pfb_chsel`` generate-loop instances, 4 more ``ID[4-7]_REG`` ports, and 4 + more ``m[4-7]_axis_tdata`` outputs. +* ``ddsprod_v.sv`` goes from ``localparam N = 4`` (4 ``ddsprod`` instances) to + ``localparam N = 8`` (8 instances), with matching ``PINC[4-7]_REG``/ + ``POFF[4-7]_REG`` ports. +* ``axis_pfb_readout_v4.v``'s top level exposes 8 AXI-Stream masters + (``m0_axis``..``m7_axis``) instead of v3's 4, and its AXI-Lite address bus + widens from 6 bits (``s_axi_awaddr[5:0]``) to 8 bits + (``s_axi_awaddr[7:0]``) to fit the doubled register count (24 vs. 12). +* The underlying 64-channel PFB channelizer itself (``pfb.sv`` and everything + it instantiates, including the ``ssrfft_8x64`` FFT core) is **unchanged** + between v3 and v4 -- both still channelize the same 64 fixed bins from the + same ADC input; only how many of those channels can be simultaneously + routed out (and independently DDS-tuned) differs. + +So v4 is best described as *v3 with NOUT widened from 4 to 8*, not a +repackaging with identical RTL, and not an independent redesign of the +channelizer. This matches the Python driver docstring/registration exactly +(``AxisPFBReadoutV4(AxisPFBReadoutV3)`` overrides only ``NOUT = 8``, and its +docstring says *"This is identical to AxisPFBReadoutV3, but with 8 outputs +instead of 4"*) -- a case where the Python driver's claim and the RTL diff +independently agree. + +One thing this investigation could **not** determine, and does not guess at: +*why* v4 exists as a separate IP core (with its own VLNV, +``QICK:QICK:axis_pfb_readout_v4:1.0``) rather than v3 simply being +re-synthesized with a ``NOUT`` parameter -- unlike ``N`` (PFB channel count, +a real Verilog ``parameter`` on ``axis_pfb_readout_v3``/``v4``'s module +header), ``NOUT`` is a ``localparam`` hard-coded inside ``pfb_top.sv``/ +``ddsprod_v.sv``, not exposed as a module parameter, so making it +configurable would have required a nontrivial refactor of the fixed-width +port lists and register maps in both files -- forking to a new top-level +(and giving it a new IP core name) was evidently the path taken instead, but +no comment or commit message read in this session confirms that reasoning. + +4.2 Register Map +^^^^^^^^^^^^^^^^^ + +Same layout as v3, doubled: ``ID[0-7]_REG`` (16 bits each) at offsets 0-7, +then ``PINC[0-7]_REG``/``POFF[0-7]_REG`` (32 bits each) pairs at offsets +8/9, 10/11, ... 22/23 -- 24 registers total, matching +``AxisPFBReadoutV4``'s inherited ``_init_config()`` (which just runs v3's +same offset-generation loop with ``self.NOUT = 8``) and the widened 8-bit +AXI-Lite address bus in ``axis_pfb_readout_v4.v``/``axi_slv.v``. + +-------------------------------------------------------------------- +5. Python Interface +-------------------------------------------------------------------- + +5.1 ``AbsPFBReadout`` (shared base class) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +``qick.drivers.readout.AbsPFBReadout(SocIP, AbsReadout)`` factors out what +all three versions have in common: + +* ``B_DDS = 32`` and a fixed ``IQ_OFFSET = -0.5`` -- the driver source + comments this as *"based on testing this seems like it might really be some + weird value like -0.48, even though this makes no sense"*, i.e. an + empirically-determined constant, not one derived from a specific rounding + operation in RTL (unlike, say, ``axis_dyn_readout_v1``'s offset -- see + :doc:`readout_dynamic`). +* ``_init_config()`` derives ``DOWNSAMPLING = NCH // 2`` (the PFB's + 50%-overlap decimation) and ``CH_OFFSET = NCH // 2`` (the index of the PFB + channel centered at DC), and publishes ``pfb_nch``, ``pfb_nout``, + ``pfb_ch_offset``, ``pfb_dds_on_output`` into ``self.cfg`` for + ``QickConfig``/``calc_ro_regs()`` to use. +* ``configure_connections()`` traces the block's ``s_axis`` input back + through the RFDC (or, on boards with dual-ADC combiners like ZCU111/ + RFSoC4x2, through an intervening ``axis_combiner``) to determine which + physical ADC feeds this readout. +* ``configure()`` further divides ``f_dds``/multiplies ``fdds_div`` by + ``DOWNSAMPLING``, since the DDS's effective tuning range is reduced by both + the RFDC's decimation and the PFB's own 50%-overlap decimation. +* ``set_ch(f, out_ch, sel='product', gen_ch=None, phase=0)`` is the + debug-only, direct-register equivalent of ``declare_readout()`` for a PFB + output -- not used in normal programs, same caveat as + :meth:`.AbsReadout.set_all` on the single-tone readouts. + +5.2 Version-specific subclasses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Driver class attributes (from ``qick_lib/qick/drivers/readout.py``) + :header-rows: 1 + :widths: 26 16 16 16 26 + + * - + - ``NCH`` + - ``NOUT`` + - ``DDS_ON_OUTPUT`` + - ``HAS_OUTSEL`` + * - ``AxisPFBReadoutV2`` + - 8 (class constant) + - 4 (class constant) + - ``False`` + - ``True`` + * - ``AxisPFBReadoutV3`` + - read from the bitstream's ``N`` generic (``description['parameters']['N']``) + - 4 (class constant) + - ``True`` + - ``False`` + * - ``AxisPFBReadoutV4`` + - inherited from V3 (bitstream generic) + - 8 (class constant, only override vs. V3) + - ``True`` (inherited) + - ``False`` (inherited) + +``AxisPFBReadoutV2.set_freq_int(cfg)`` writes two registers per call: the +output's ``ch%dsel_reg`` (which of the 8 channels to route) and that +channel's ``freq%d_reg`` (the DDS tuning word) -- note this *overwrites* the +shared per-channel DDS setting, so the driver's own comment warns *"it's +assumed that channel collisions have already been checked ... a collision +will break the previously set channel"* if two outputs are pointed at the +same input channel with different frequencies. + +``AxisPFBReadoutV3.set_freq_int(cfg)`` validates ``pfb_ch``/``out_ch`` are +in-range, computes the ``(packet, index)`` pair for the ``ID`` register from +``pfb_ch``, and writes ``id``/``freq``/``phase`` for that output -- since the +DDS is per-output here, there's no collision risk analogous to v2's, and the +driver comment says so explicitly ("No need to check for collisions as they +are all truly independent"). ``AxisPFBReadoutV4`` inherits this method +unchanged; only ``NOUT`` differs. + +.. code-block:: python + :caption: Debug-only direct configuration of one PFB output (normally done via declare_readout()) + + from qick import * + + soc = QickSoc() + pfb = soc.readouts[i] # an AxisPFBReadoutV3 or AxisPFBReadoutV4 instance + + # Route PFB channel 5 to output 2, demodulated at 100 MHz. + pfb.set_ch(f=100.0, out_ch=2, sel='product') + +Normal QICK programs never call ``set_ch``/``set_freq_int`` directly -- +:meth:`.QickProgram.declare_readout` picks the PFB channel/output assignment +and frequency for you (via ``QickConfig.calc_ro_regs()``), exactly as +described in :doc:`index`'s Python Interface section for the single-tone +readouts. + +.. _readout-pfb-zcu216: + +-------------------------------------------------------------------- +6. Real-World Usage: ZCU216 "tpv2_std" +-------------------------------------------------------------------- + +The flagship ZCU216 board design (``firmware/Top/216/tpv2_std/bd_2023-1.tcl``, +tracked on branch ``feature/114-integrate-hog-fusesoc``) instantiates +**one** ``axis_pfb_readout_v4_0`` (VLNV ``QICK:QICK:axis_pfb_readout_v4:1.0``) +alongside **two** ``axis_dyn_readout_v1`` instances (:doc:`readout_dynamic`) +and **ten** ``axis_avg_buffer`` instances (:doc:`avg_buffer`) -- tracing the +block-diagram connections (``connect_bd_intf_net``) confirms exactly how that +10 breaks down, rather than assuming it's all attributable to one core: + +* ``axis_pfb_readout_v4_0`` exposes 8 outputs, ``m0_axis``..``m7_axis`` (its + full ``NOUT=8``, confirmed by 8 separate + ``axis_pfb_readout_v4_0_m[0-7]_axis`` nets in the .tcl). Each output feeds + its own ``axis_broadcaster`` (``axis_broadcaster_2`` through + ``axis_broadcaster_9``), which in turn drives its own dedicated + ``axis_avg_buffer`` (``axis_avg_buffer_2`` through ``axis_avg_buffer_9``) -- + **8 of the 10 avg_buffers on this board are fed by the single PFB readout + instance**, one per PFB output channel. +* The remaining 2 avg_buffers (``axis_avg_buffer_0``, ``axis_avg_buffer_1``) + are fed the same way (via their own broadcasters, + ``axis_broadcaster_0``/``axis_broadcaster_1``) by the two + ``axis_dyn_readout_v1`` instances' single ``m1_axis`` outputs. + +So the buffer count (10) is not a property of "how many readout instances +there are" (there are 3: 1 PFB + 2 dynamic) -- it's driven by *output ports*: +1 (per dynamic readout) x 2 + 8 (per PFB readout, its full NOUT) x 1 = 10. +This is exactly the PFB-vs-single-tone distinction from +:ref:`readout-pfb-general` above made concrete on real, currently-shipping +hardware: one PFB readout instance does the job of 8 single-tone readout + +buffer pairs, at the cost of those 8 channels sharing one fixed 64-way +channelizer (they can only demodulate PFB bins independently fine-tuned by +up to +/- half a bin, not arbitrary frequencies across the whole band +simultaneously). + +The PFB readout's AXI-Lite control port (``axis_pfb_readout_v4_0/s_axi``) is +mapped in the address space at offset ``0x000400101000``, range ``0x1000`` +(the .tcl's ``assign_bd_address`` call) -- a full 4 KiB page is reserved even +though the register map itself only spans 24 x 4 = 96 bytes (see +:ref:`readout-pfb-general`'s address-width comparison), consistent with the +AXI-Lite page-granularity convention used elsewhere in this address map +(not specific to the PFB readout). + +-------------------------------------------------------------------- +Related Documentation +-------------------------------------------------------------------- + +* :doc:`index` -- readout system overview; note its own single-tone + framing applies to :doc:`readout_v2` and :doc:`readout_dynamic`, not to + the PFB family described here. +* :doc:`readout_v2` -- the single-tone, PYNQ-register-configured readout + the PFB readouts are architecturally different from (one tunable + demodulator vs. a fixed multi-channel bank). +* :doc:`readout_dynamic` -- the tProc-configured single-tone readouts + (``axis_readout_v3``/``axis_dyn_readout_v1``) that share the ZCU216 board + design with ``axis_pfb_readout_v4`` (see :ref:`readout-pfb-zcu216`). +* :doc:`avg_buffer` -- the buffer IP downstream of each PFB output; a PFB + readout with NOUT outputs typically drives NOUT separate avg_buffer + instances, as shown on the ZCU216. +* :doc:`/topics/freq_matching` -- keeping generator and readout frequencies + in sync. diff --git a/docs/source/firmware/readouts/readout_v2.rst b/docs/source/firmware/readouts/readout_v2.rst new file mode 100644 index 000000000..194a4ac90 --- /dev/null +++ b/docs/source/firmware/readouts/readout_v2.rst @@ -0,0 +1,173 @@ +======================================================== +Readout v2 (axis_readout_v2) - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +**axis_readout_v2** is the digital down-converter (DDC) IP that sits between +the RFDC's ADC output and :doc:`avg_buffer`. It is the readout used on +tProc-v1-style single-tone (non-multiplexed, non-PFB) readout channels. The +module lives under ``firmware/ip/axis_readout_v2/`` and is exposed to Python +through ``qick.drivers.readout.AxisReadoutV2``. + +-------------------------------------------------------------------- +1. General Description +-------------------------------------------------------------------- + +``axis_readout_v2`` mixes the ADC's real-valued input samples down to +baseband with an on-chip DDS, then applies an FIR anti-aliasing filter and +decimates by 8 before handing the complex (I, Q) stream to +:doc:`avg_buffer`. Unlike the newer PFB-based readouts, it demodulates a +single, software-tunable frequency (no simultaneous multi-tone channelizer). + +The datapath, in order: + +1. **down_conversion** (``down_conversion.v``) -- complex multiply of the + real ADC input against a local DDS tone (``dds_compiler_0``), 16 parallel + lanes (``N_DDS=16`` samples/clock, matching the RFDC's parallel output). +2. **down_conversion_fir** (``down_conversion_fir.v``) -- FIR low-pass + (``fir_compiler_0``, fixed coefficients compiled into the bitstream) plus + decimation by 8. +3. Output to ``axis_avg_buffer``'s ``s_axis``. + +.. figure:: /images/firmware/readout_v2-blocks.svg + :align: center + :width: 85% + + Top-level block diagram of the current fusesoc-ported RTL + (``readout_top_ro_v2.sv`` / ``down_conversion_ro_v2.sv``), verified + directly against that source. **Note:** this reflects ``N_DDS=8`` + (fixed by the shared, reused ``fir_decim8_dynro`` block) and a 3-way + ``OUTSEL`` mux (product/dds/raw ADC) inside ``down_conversion_ro_v2``, + which differs from the ``N_DDS=16`` / no-``OUTSEL`` description above + -- that prose describes the pre-fusesoc-port ``firmware/ip/ + axis_readout_v2/`` RTL and hasn't been reconciled with the port yet. + +-------------------------------------------------------------------- +2. Frequency/Output Control (``ctrl.sv``) +-------------------------------------------------------------------- + +Register writes don't take effect directly -- ``ctrl.sv`` packs +``freq``/``phase``/``nsamp``/``outsel``/``mode`` into a single 83-bit +descriptor pushed through a FIFO (driven by the ``WE_REG`` pulse), then +drains that FIFO into the N_DDS DDS phase accumulators: + +.. list-table:: Descriptor fields (FIFO word, 83 bits) + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Width + - Description + * - ``freq`` + - 32 bits + - DDS tuning word (downconversion frequency). + * - ``phase`` + - 32 bits + - DDS phase offset. + * - ``nsamp`` + - 16 bits + - Sample count used when ``mode=0`` (see below). + * - ``outsel`` + - 2 bits + - Output source (see register map). + * - ``mode`` + - 1 bit + - 0: run for ``nsamp`` samples then stop. 1: run continuously + (periodic). + +The complex multiply itself: each of the 16 lanes takes one 16-bit real ADC +sample and multiplies it against that lane's complex DDS output +(``dds_compiler_0``, 32-bit {Q,I}), producing a 32-bit signed product per +component, rounded (add ``2**15``, keep bits ``[31:16]``) back down to 16-bit +I/Q before the output mux -- this is the "product" ``outsel`` value. + +-------------------------------------------------------------------- +3. Register Map +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Register + - Width + - Description + * - ``FREQ_REG`` + - 32 bits + - Downconversion DDS frequency. + * - ``PHASE_REG`` + - 32 bits + - DDS phase offset. + * - ``NSAMP_REG`` + - 16 bits + - Sample count for one-shot mode (``MODE_REG=0``); ignored in periodic + mode. + * - ``OUTSEL_REG`` + - 2 bits + - 0: product (mixer output, the normal downconverted signal). 1: dds + (raw local oscillator tone, for calibration/debugging). 2: input + (bypass -- the raw ADC sample, delay-matched to the other paths). + * - ``MODE_REG`` + - 1 bit + - 0: NSAMP (stop after ``NSAMP_REG`` samples). 1: Periodic (free-running + tone). + * - ``WE_REG`` + - 1 bit (pulse) + - Toggling this 0->1->0 pushes the current FREQ/PHASE/NSAMP/OUTSEL/MODE + register values into the descriptor FIFO as one atomic update. + +-------------------------------------------------------------------- +4. Key Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Parameter + - Value + - Notes + * - Downsampling ratio + - 8 + - Fixed FIR decimation; exposed to Python as ``DOWNSAMPLING = 8`` on + the driver class. + * - DDS/phase width + - 32 bits + - ``B_DDS = B_PHASE = 32`` on the driver. + * - IQ offset + - 0 + - Unlike the muxed/PFB readouts, ``axis_readout_v2`` introduces no + fixed I/Q rounding offset (``IQ_OFFSET = 0.0`` -- see + :meth:`.AcquireMixin._ro_offset`). + * - tProc control + - none + - This readout is PYNQ/software-configured only; it has no + ``tproc_ctrl`` connection (unlike the dynamic readouts, see + :doc:`readout_dynamic`). + +-------------------------------------------------------------------- +5. Python Interface +-------------------------------------------------------------------- + +As with the buffer (:doc:`avg_buffer`), normal programs configure this +readout via :meth:`.QickProgram.declare_readout` (``freq``, ``phase``, +``sel``, ``gen_ch``), not by writing registers directly -- see +:doc:`index`'s Python Interface section for the full +``declare_readout()``/``trigger()``/``acquire()`` workflow. +``AxisReadoutV2.set_all()``/``set_all_int()`` exist for direct register +access but are explicitly for debugging: *"Normally the [readout] is +configured based on parameters supplied in QickProgram.declare_readout()."* + +Related Documentation +---------------------- + +* :doc:`index` -- readout system overview and the normal + ``declare_readout()`` workflow. +* :doc:`avg_buffer` -- the buffer downstream of this readout's output. +* :doc:`readout_dynamic` -- the tProc-configured readouts + (``axis_readout_v3``/``axis_dyn_readout_v1``), for comparison. +* :doc:`/topics/freq_matching` -- keeping generator and readout frequencies + in sync. diff --git a/docs/source/firmware/support_cores.rst b/docs/source/firmware/support_cores.rst new file mode 100644 index 000000000..c5688b326 --- /dev/null +++ b/docs/source/firmware/support_cores.rst @@ -0,0 +1,459 @@ +======================================================== +Support & Utility Cores - QICK Firmware +======================================================== + +.. contents:: + :local: + :depth: 2 + +The three cores on this page -- **axis_constant_iq**, **axis_set_reg**, and +**axis_streamer** -- are small, single-purpose utility blocks that live under +``firmware/ip/`` alongside the readout and signal-generator families but +don't belong to either one. Each is 1-7 RTL files, so rather than a +full page per core (as :doc:`readouts/readout_v2`, :doc:`generators/sg_v6`, etc. get), they +share this one page, one section each. + +.. figure:: /images/firmware/support_cores-blocks.svg + :align: center + :width: 85% + + Block diagram of all three cores -- each is small enough that this is + effectively its full internal structure, not a simplification. + +.. _support-constant-iq: + +------------------------------------------------------------------------ +1. axis_constant_iq +------------------------------------------------------------------------ + +1.1 General Description +^^^^^^^^^^^^^^^^^^^^^^^^ + +**axis_constant_iq** (``firmware/ip/axis_constant_iq/``, top module +``axis_constant_iq.sv``) is the simplest signal source in QICK: it holds one +constant, software-written complex (I, Q) value and streams it out on +``m_axis`` forever, once armed. As the Python driver's docstring puts it, it +"plays a constant IQ value, which gets mixed with the DAC's built-in +oscillator" -- the block itself has **no DDS and no envelope memory**. All of +the RF frequency content comes from the RFDC's own digital upconverter +(configured through :meth:`.AbsSignalGen.set_mixer_freq`, inherited by this +driver); ``axis_constant_iq`` only supplies the static baseband amplitude/ +phase that gets mixed up to that frequency. + +This is a deliberate contrast with the two arbitrary/multiplexed generator +families already documented: + +* :doc:`generators/sg_v6` synthesizes a shaped, arbitrary envelope from a DDS + BRAM + waveform engine, sequenced pulse-by-pulse by tProc-issued descriptors. +* :doc:`generators/sg_mux8` has no envelope memory either, but does have its own + per-tone DDS (up to 8 simultaneous tones), and is still sequenced by the + tProcessor (a streamed mask + duration selects which tones play, when). + +``axis_constant_iq`` has neither an internal DDS nor a connection to the +tProcessor's timed-pulse mechanism at all -- there is no ``s_axis`` pulse +input on this block's port list. It is a pure "set and forget" AXI-Lite +block: software writes an I/Q value once (or occasionally), and the DAC +continuously outputs it until the next write. + +Internally, ``axis_constant_iq.sv`` instantiates two small support blocks +also under ``src/``: + +* ``axil_slv.sv`` -- a generic, reusable AXI4-Lite register-file slave + (parametrized address/data width and register count). The same block is + reused, independently instantiated, by :ref:`axis_streamer ` + below. +* ``sync_nxm.sv`` -- a small parametrized (N-stage, M-bit) synchronizer, + used here to cross the single-bit ``WE_REG`` "commit" strobe from the + AXI-Lite clock domain (``s_axi_aclk``) into the sample-stream clock domain + (``m_axis_aclk``). + +Two other files in ``src/`` (``axi4_lite_if.sv``, ``axi4_stream_if.sv``) are +SystemVerilog verification interfaces (their headers describe them as +testbench-oriented, SVUnit-style protocol tasks) -- they are not instantiated +anywhere in ``axis_constant_iq.sv``'s actual datapath. + +**Update sequencing.** Software writes the new I and Q values to +``REAL_REG``/``IMAG_REG``, then pulses ``WE_REG`` (write 1, then write 0). +On the ``m_axis_aclk`` side, the synchronized ``we`` signal is edge-detected +(``we_int = ~we_r & we``); on that one-cycle pulse, ``real_r``/``imag_r`` are +loaded from the register values and ``m_axis_tvalid`` latches high for good. +From then on ``m_axis_tdata`` continuously presents ``{imag_r, real_r}``, +identically replicated across all ``NUM_CHANNELS`` parallel output lanes, and +``m_axis_tlast`` is tied to 0 -- this is a genuinely unframed, continuous +stream, never packetized. + +1.2 Parameters and Register Map +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Synthesis parameters + :header-rows: 1 + :widths: 20 15 55 + + * - Parameter + - Default + - Description + * - ``IQ_WIDTH`` + - 16 + - Bit width of each of the I and Q components. + * - ``NUM_CHANNELS`` + - 4 + - Number of identical, parallel ``{Q,I}`` output lanes packed into + ``m_axis_tdata`` (``2*IQ_WIDTH*NUM_CHANNELS`` bits wide total). All + lanes always carry the same value -- there is no per-lane + configuration. + +Registers are exposed through the generic ``axil_slv`` block with +``AXI_DW=32``, ``AXI_AW=6``, ``NUM_REGS=4`` -- 4 word-aligned, 32-bit +registers at byte offsets ``0x00``-``0x0C``: + +.. list-table:: Register map (word index, byte offset) + :header-rows: 1 + :widths: 18 12 12 15 43 + + * - Register + - Index + - Offset + - Width + - Description + * - ``REAL_REG`` + - 0 + - 0x00 + - ``IQ_WIDTH`` bits (signed), low bits of the 32-bit word + - I component, latched into ``real_r`` on the next ``WE_REG`` strobe. + * - ``IMAG_REG`` + - 1 + - 0x04 + - ``IQ_WIDTH`` bits (signed), low bits of the 32-bit word + - Q component, latched into ``imag_r`` on the next ``WE_REG`` strobe. + * - ``WE_REG`` + - 2 + - 0x08 + - bit ``[0]`` + - Write-enable/commit strobe. Edge-detected in the ``m_axis_aclk`` + domain, so software must write 1 then 0 to commit a new I/Q value. + * - (reserved) + - 3 + - 0x0C + - -- + - Tied to 0 in the RTL (``slv_regs_i[3] = '0``); unused. + +1.3 Python Interface +^^^^^^^^^^^^^^^^^^^^^ + +``axis_constant_iq`` is exposed through +``qick.drivers.generator.AxisConstantIQ``, a subclass of +:class:`.AbsSignalGen` (``bindto = ['user.org:user:axis_constant_iq:1.0', +'QICK:QICK:axis_constant_iq:1.0']``, ``HAS_MIXER = True``). Its +``REGISTERS`` dict maps directly onto the word indices above (``real_reg: +0, imag_reg: 1, we_reg: 2``). + +.. code-block:: python + + from qick import * + + soc = QickSoc() + gen = soc.ci_gens[0] # AxisConstantIQ instance for this channel + + gen.set_nyquist(2) # inherited from AbsSignalGen + gen.set_mixer_freq(500.0) # MHz -- picks the actual output frequency + gen.set_iq(i=1.0, q=0.0) # normalized signed gain, -1..1 + +``set_iq(i, q)`` scales the normalized ``(i, q)`` gains (range -1 to 1) by +``MAXV = 2**15-2`` into ``np.int16`` values, writes them to +``real_reg``/``imag_reg``, and calls ``update()``, which performs exactly the +1-then-0 write to ``we_reg`` that the RTL's edge detector expects. On +firmware init (``_init_firmware()``), the driver defaults both registers to +``MAXV`` (full-scale) and commits that value immediately. + +Because this generator is never sequenced by the tProcessor, the +:meth:`.AbsSignalGen.set_nyquist` and :meth:`.AbsSignalGen.set_mixer_freq` +methods it inherits must be called directly by the user -- their docstrings +note this explicitly ("You should normally only call this method directly +for a constant-IQ output"), since for tProc-controlled generators these are +normally invoked automatically as part of program configuration. + +------------------------------------------------------------------------ +2. axis_set_reg +------------------------------------------------------------------------ + +2.1 General Description +^^^^^^^^^^^^^^^^^^^^^^^^ + +**axis_set_reg** (``firmware/ip/axis_set_reg/src/axis_set_reg.sv``) is a +single-file, single-clock-domain "capture and hold" register. Its own header +comment describes it plainly: *"Simple register that captures AXI-Stream +slave data when tvalid is high."* There is no AXI-Lite interface and no +master AXI-Stream output -- just an AXI-Stream slave input (``s_axis_*``) +and a plain ``dout`` output port, ``DATA_WIDTH`` bits wide (default 16, per +``component.xml``). + +The RTL logic is exactly as small as it sounds: + +* On every rising edge of ``s_axis_aclk``, if ``s_axis_tvalid`` is high, + ``dout_r <= s_axis_tdata``; otherwise ``dout_r`` retains its previous + value. +* ``s_axis_tready`` is tied permanently to 1 -- the block never + back-pressures its master. +* ``dout`` is just ``dout_r``, combinationally. +* ``s_axis_tstrb``/``s_axis_tlast`` are present on the port list "for + completeness" (per the header comment) but are not used anywhere in the + RTL body. + +In effect, this block turns a **transient** AXI-Stream beat (valid for one +clock, whenever its master chooses to push one) into a **stable, level-held** +output that persists until the next beat overwrites it. + +2.2 Role in the tProc v1 output chain +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There is **no dedicated Python driver class** for ``axis_set_reg`` -- a +search of every driver module under ``qick_lib/qick/drivers/`` turns up no +``SocIP`` subclass with a ``bindto`` matching it. The block is referenced +only by its type-name string, ``"axis_set_reg"``, purely at the +hardware-metadata / connectivity-tracing level, in two places: + +* ``qick_lib/qick/ip.py``, ``HardwareMetadata.trace_trigger()`` (around line + 491-514): when this method resolves which tProc port/bit ultimately + triggers a given buffer, and the block driving that trigger is a + ``vect2bits``/``qick_vec2bit`` bit-splitter, it follows that splitter's + ``din`` input back one more hop. If *that* upstream block's type is + ``axis_set_reg``, it follows one further hop through the ``axis_set_reg``'s + own ``s_axis`` input to reach the real originating tProc output port. The + code's own comment spells out the three topologies it has to distinguish, + one of which is exactly:: + + # tproc v1 output port -> axis_set_reg -> vect2bits -> buffer + # tproc v2 data port -> vect2bits -> buffer + # tproc v3 trigger port -> buffer + +* ``qick_lib/qick/drivers/tproc.py``, + ``AxisTProc64x32_x8.configure_connections()`` (around line 130-148): while + enumerating tProc v1's 8 output ports (``m1_axis``..``m8_axis``; port 0 is + reserved for the DMA), for each output found connected to an + ``axis_set_reg``, the code traces that ``axis_set_reg``'s ``dout`` signal + onward and inspects its individual bit taps (``dout0``..``dout15``) to see + whether any of them fan out directly to a top-level FPGA pin. Any pin found + this way is recorded in ``cfg['output_pins']``. + +Combining what the RTL actually does with what these two call sites are +looking for (as far as is verifiable from source, without an actual built +design's HWH file to inspect): ``axis_set_reg`` sits between a tProc v1 +output port and a downstream ``vect2bits`` bit-splitter (or, in some +designs, drives FPGA output pins directly). A tProc v1 output-port write is +a single-beat AXI-Stream transaction -- transient by nature. ``axis_set_reg`` +captures that value and holds it as a stable level on ``dout``, which +``vect2bits`` then decomposes into individual, level-held bits -- used, for +example, to hold a buffer's trigger input high, or to drive a digital output +pin, in between tProc writes. The three-way comment in ``ip.py`` also makes +clear this ``axis_set_reg`` step is specific to tProc v1: tProc v2 drives +``vect2bits`` directly from a data port (no intervening register), and +tProc v3 has dedicated trigger ports that go straight to a buffer. + +**There is no Python API for this block**, and none is needed for its role: +the metadata layer (``qick.ip``/``qick.drivers.tproc``) discovers the +connectivity automatically, by block type name, when the overlay is loaded -- +not through a per-block PYNQ register driver. A QICK user does not interact +with ``axis_set_reg`` directly; they program tProc v1 output-port writes, and +the hardware-metadata tracer figures out which trigger or pin those writes +end up driving. + +.. _support-streamer: + +------------------------------------------------------------------------ +3. axis_streamer +------------------------------------------------------------------------ + +3.1 General Description +^^^^^^^^^^^^^^^^^^^^^^^^ + +**axis_streamer** (``firmware/ip/axis_streamer/``) is, per its own header +comment, *"AXI4-Stream buffer with an AXI4-Lite control interface, for DMA +readout of a captured stream."* The top module (``axis_streamer.sv``) wraps +two pieces: + +* ``axil_slv.sv`` -- the same generic AXI4-Lite register-file block used by + :ref:`axis_constant_iq ` above (independently + instantiated here, not shared logic). +* ``streamer.sv`` -- the actual datapath core. + +``streamer.sv`` captures an input AXI-Stream (``s_axis``, the "s" clock +domain) into a dual-clock asynchronous FIFO (``fifo_async.sv``, built on a +Xilinx ``xpm_fifo_async`` primitive), and re-emits it on a separate output +AXI-Stream (``m_axis``, the "m" clock domain) in fixed-size packets of +``nsamp_reg`` beats, each terminated by ``tlast``. Its header comment states +the intended use case explicitly: a free-running, un-stallable source (e.g. +an ADC feeding this block) on the input side, and a DMA draining the output +side. + +**Clock-domain crossing.** The FIFO itself (an XPM async FIFO) carries the +actual sample data across domains. Every control/status signal that also +needs to cross is individually resynchronized through +``cdc_bit_sync.sv`` (a parametrized N-stage bit synchronizer, 2 stages by +default): ``start_reg`` into the m domain, ``flush_reg`` into the m domain, +the run-state handshake in both directions, and each of the 6 ``STATUS`` +bits individually into the AXI-Lite domain. The RTL comments are explicit +that these are deliberately **not coherent as a group** -- each bit/signal +is only individually meaningful, which is why the register interface exposes +decoded status flags rather than a raw FSM state. + +**Control FSM** (``streamer.sv``): ``IDLE_ST -> RUN_ST -> WAIT_END_ST -> +END_ST``, gated by ``start_reg`` (a level, not a pulse -- dropping it while +running ends the run) and ``mode_reg`` (0 = one-shot: stop after one packet; +1 = continuous: back-to-back packets). ``flush_reg`` controls what happens to +a partial packet when the run ends: 1 drains it (final beat carries +``tlast``), 0 discards it (though a beat already presented on the bus is +still allowed to complete its handshake -- the RTL comments call this out as +a deliberate deviation from an earlier implementation, to stay AXI-legal). + +**Backpressure.** ``s_axis_tready = ~fifo_full``, deliberately *not* gated by +run state -- the header comment describes the block as "a passive tap" that +must never stall its producer. Beats offered while the block is idle are +silently dropped and are **not** counted toward ``lost_reg`` (only beats +dropped while actually running are). + +**What ``LOST``/``OVERFLOW`` actually count.** ``lost_reg`` counts *cycles* +in the s-domain during a run where the source offered a beat while the FIFO +was full -- not distinct beats. The RTL comments are careful to note this +only equals "samples dropped" for a source that cannot be stalled (like a +free-running ADC feeding a DMA); for a well-behaved AXI-Stream master that +holds ``tvalid`` across a stall, the same beat is counted repeatedly and +nothing is actually lost -- in that configuration the counter measures +backpressure, not loss. + +3.2 Register Map +^^^^^^^^^^^^^^^^^ + +Registers are exposed through ``axil_slv`` with ``AXI_DW=32``, ``AXI_AW=6``, +``NUM_REGS=6``. The map below is taken directly from ``axis_streamer.sv``'s +own header comment, which documents it explicitly: + +.. list-table:: Register map (byte-addressed, 32-bit registers) + :header-rows: 1 + :widths: 15 12 10 15 48 + + * - Register + - Offset + - R/W + - Width + - Description + * - ``START`` + - 0x00 + - RW + - bit ``[0]`` + - Block enable. LEVEL, not a pulse. + * - ``FLUSH`` + - 0x04 + - RW + - bit ``[0]`` + - 1 = drain a partial packet with ``tlast`` on stop; 0 = discard it. + * - ``MODE`` + - 0x08 + - RW + - bit ``[0]`` + - 0 = one-shot; 1 = continuous. + * - ``NSAMP`` + - 0x0C + - RW + - ``[31:0]`` + - Beats per packet. 0 keeps the block idle. + * - ``LOST`` + - 0x10 + - RO + - ``[31:0]`` + - Beats dropped while running, latched when the block stops. + * - ``STATUS`` + - 0x14 + - RO + - ``[5:0]`` + - See status bits below. + +.. list-table:: STATUS bits + :header-rows: 1 + :widths: 10 20 70 + + * - Bit + - Name + - Meaning + * - 0 + - ``RUNNING`` + - The core is capturing and streaming. + * - 1 + - ``DONE`` + - One-shot packet finished; clear ``START`` to return to idle. + * - 2 + - ``IDLE`` + - Safe to reconfigure; ``LOST`` holds the last run's count. + * - 3 + - ``OVERFLOW`` + - Sticky: samples were lost during this or the last run. Cleared + automatically when a new run starts. + * - 4 + - ``FIFO_FULL`` + - Instantaneous, diagnostic only. + * - 5 + - ``FIFO_EMPTY`` + - Instantaneous, diagnostic only. + +The header comment also documents the intended software sequence: write +``NSAMP``/``MODE``/``FLUSH`` only while ``STATUS.IDLE`` is set; write +``START=1``; wait for ``tlast`` (one-shot) or run until you choose to stop +(continuous); write ``START=0``; then **poll** ``STATUS.IDLE`` before +reading ``LOST`` -- stopping is not instantaneous, since ``START`` crosses +several flops, so the core keeps streaming for a few cycles after the write +retires. + +3.3 Python Interface -- is ``DataStreamer`` the driver? +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**No.** This is worth stating explicitly, because the name similarity is +misleading and the relationship is not 1:1. + +Verified by searching every driver module under ``qick_lib/qick/drivers/`` +(``generator.py``, ``readout.py``, ``tproc.py``, ``peripherals.py``, +``xcom.py``): none of them defines a ``SocIP``/``DummyIP`` subclass with a +``bindto`` referencing ``axis_streamer``. The only Python artifact anywhere +under ``qick_lib`` that carries the ``axis_streamer`` name is +``qick_lib/qick/rdl_pkgs/axis_streamer_regmap_pkg/`` -- an auto-generated +PeakRDL register-model wrapper (``reg_model/``, ``sim/``, ``tests/``), +generated from an RDL register-map source elsewhere in the repo. That is a +register-level Python model intended for RTL simulation/testbench use (it +ships with its own ``SimTestCase``/``TestCase`` classes), not a PYNQ overlay +driver wired into ``QickSoc``. A repo-wide search also found no reference to +``axis_streamer`` in ``qick_lib/qick/soc.py`` or in any board block-design +file -- so, as far as this repository shows, the core doesn't currently +appear wired into a shipped QICK board bitstream either. + +``qick.streamer.DataStreamer`` (``qick_lib/qick/streamer.py``) is a +completely different, higher-level, **purely software** abstraction: a +background Python ``Thread`` that repeatedly calls ``soc.get_tproc_counter()`` +and ``soc.get_accumulated()`` to pull already-accumulated I/Q points out of +the averaging buffer (:doc:`readouts/avg_buffer`) while a tProc program runs, +queuing the results for the main thread to consume via ``poll_data()``. It +never touches an MMIO register map and never references any of +``axis_streamer``'s ``START``/``FLUSH``/``MODE``/``NSAMP``/``LOST``/ +``STATUS`` registers -- its "streaming" is a software polling loop layered +on top of :doc:`readouts/avg_buffer` and the tProcessor's shot counter, with no +dependency on the ``axis_streamer`` IP block at all. + +So: as verified in this codebase, ``axis_streamer`` currently has no +register-level Python counterpart beyond the auto-generated simulation +register model, and ``DataStreamer`` is not that counterpart -- it solves an +unrelated, software-side data-collection problem that happens to share the +word "stream." Driving ``axis_streamer`` from Python today would require +writing a new ``SocIP`` subclass, following the pattern used by +:ref:`axis_constant_iq ` above or the readout/generator +drivers. + +Related Documentation +---------------------- + +* :doc:`generators/sg_v6` -- arbitrary-envelope signal generator; contrast with + ``axis_constant_iq``'s lack of any DDS or envelope memory. +* :doc:`generators/sg_mux8` -- fixed-tone multiplexed generator; also contrasted with + ``axis_constant_iq`` above. +* :doc:`readouts/avg_buffer` -- the buffer that :class:`.DataStreamer` actually + polls, as opposed to ``axis_streamer``. +* :doc:`readouts/readout_dynamic` -- another block whose Python-side object is + constructed manually rather than through the normal PYNQ overlay-scan + mechanism, for comparison with ``axis_set_reg``'s pure metadata-tracing + role. diff --git a/docs/source/images/firmware/dyn_readout-blocks.svg b/docs/source/images/firmware/dyn_readout-blocks.svg new file mode 100644 index 000000000..d101dbb72 --- /dev/null +++ b/docs/source/images/firmware/dyn_readout-blocks.svg @@ -0,0 +1,751 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/firmware-overview.svg b/docs/source/images/firmware/firmware-overview.svg new file mode 100644 index 000000000..a37e9f0c5 --- /dev/null +++ b/docs/source/images/firmware/firmware-overview.svg @@ -0,0 +1,471 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/kidsim-blocks.svg b/docs/source/images/firmware/kidsim-blocks.svg new file mode 100644 index 000000000..f57503b50 --- /dev/null +++ b/docs/source/images/firmware/kidsim-blocks.svg @@ -0,0 +1,1002 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/mr_buffer_et-blocks.svg b/docs/source/images/firmware/mr_buffer_et-blocks.svg new file mode 100644 index 000000000..017e9e76e --- /dev/null +++ b/docs/source/images/firmware/mr_buffer_et-blocks.svg @@ -0,0 +1,838 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/readout_pfb_v2-blocks.svg b/docs/source/images/firmware/readout_pfb_v2-blocks.svg new file mode 100644 index 000000000..39c81178a --- /dev/null +++ b/docs/source/images/firmware/readout_pfb_v2-blocks.svg @@ -0,0 +1,791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/readout_pfb_v3-blocks.svg b/docs/source/images/firmware/readout_pfb_v3-blocks.svg new file mode 100644 index 000000000..daf21bfa2 --- /dev/null +++ b/docs/source/images/firmware/readout_pfb_v3-blocks.svg @@ -0,0 +1,792 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/readout_v2-blocks.svg b/docs/source/images/firmware/readout_v2-blocks.svg new file mode 100644 index 000000000..d6b1d0d6f --- /dev/null +++ b/docs/source/images/firmware/readout_v2-blocks.svg @@ -0,0 +1,882 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/sg_int4_v2-blocks.svg b/docs/source/images/firmware/sg_int4_v2-blocks.svg new file mode 100644 index 000000000..a192ad8da --- /dev/null +++ b/docs/source/images/firmware/sg_int4_v2-blocks.svg @@ -0,0 +1,516 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/sg_mixmux8-blocks.svg b/docs/source/images/firmware/sg_mixmux8-blocks.svg new file mode 100644 index 000000000..88423ba73 --- /dev/null +++ b/docs/source/images/firmware/sg_mixmux8-blocks.svg @@ -0,0 +1,815 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/sg_mux8-blocks.svg b/docs/source/images/firmware/sg_mux8-blocks.svg new file mode 100644 index 000000000..4f5ed8a2f --- /dev/null +++ b/docs/source/images/firmware/sg_mux8-blocks.svg @@ -0,0 +1,750 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/sg_v4-blocks.svg b/docs/source/images/firmware/sg_v4-blocks.svg new file mode 100644 index 000000000..c16d4fe16 --- /dev/null +++ b/docs/source/images/firmware/sg_v4-blocks.svg @@ -0,0 +1,810 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/sg_v6-blocks.svg b/docs/source/images/firmware/sg_v6-blocks.svg new file mode 100644 index 000000000..5ac480a32 --- /dev/null +++ b/docs/source/images/firmware/sg_v6-blocks.svg @@ -0,0 +1,505 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/support_cores-blocks.svg b/docs/source/images/firmware/support_cores-blocks.svg new file mode 100644 index 000000000..26bbafbff --- /dev/null +++ b/docs/source/images/firmware/support_cores-blocks.svg @@ -0,0 +1,1185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/source/images/firmware/tprocv2-blocks.svg b/docs/source/images/firmware/tprocv2-blocks.svg new file mode 100644 index 000000000..7a4992d66 --- /dev/null +++ b/docs/source/images/firmware/tprocv2-blocks.svg @@ -0,0 +1,1363 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/papers/QICKpapers_count_Jan26.png b/docs/source/images/papers/QICKpapers_count_Jan26.png similarity index 100% rename from docs/images/papers/QICKpapers_count_Jan26.png rename to docs/source/images/papers/QICKpapers_count_Jan26.png diff --git a/docs/images/papers/QICKpapers_count_May26.png b/docs/source/images/papers/QICKpapers_count_May26.png similarity index 100% rename from docs/images/papers/QICKpapers_count_May26.png rename to docs/source/images/papers/QICKpapers_count_May26.png diff --git a/docs/images/papers/QICKpapers_count_Sep26.png b/docs/source/images/papers/QICKpapers_count_Sep26.png similarity index 100% rename from docs/images/papers/QICKpapers_count_Sep26.png rename to docs/source/images/papers/QICKpapers_count_Sep26.png diff --git a/docs/images/papers/QICKpapers_qubit_types_Jan26.png b/docs/source/images/papers/QICKpapers_qubit_types_Jan26.png similarity index 100% rename from docs/images/papers/QICKpapers_qubit_types_Jan26.png rename to docs/source/images/papers/QICKpapers_qubit_types_Jan26.png diff --git a/docs/images/papers/QICKpapers_qubit_types_May26.png b/docs/source/images/papers/QICKpapers_qubit_types_May26.png similarity index 100% rename from docs/images/papers/QICKpapers_qubit_types_May26.png rename to docs/source/images/papers/QICKpapers_qubit_types_May26.png diff --git a/docs/images/papers/QICKpapers_qubit_types_Sep26.png b/docs/source/images/papers/QICKpapers_qubit_types_Sep26.png similarity index 100% rename from docs/images/papers/QICKpapers_qubit_types_Sep26.png rename to docs/source/images/papers/QICKpapers_qubit_types_Sep26.png diff --git a/docs/images/papers/QICKpapers_qubit_types_pie_May26.png b/docs/source/images/papers/QICKpapers_qubit_types_pie_May26.png similarity index 100% rename from docs/images/papers/QICKpapers_qubit_types_pie_May26.png rename to docs/source/images/papers/QICKpapers_qubit_types_pie_May26.png diff --git a/docs/images/papers/QICKpapers_scaling_Jan26_final.png b/docs/source/images/papers/QICKpapers_scaling_Jan26_final.png similarity index 100% rename from docs/images/papers/QICKpapers_scaling_Jan26_final.png rename to docs/source/images/papers/QICKpapers_scaling_Jan26_final.png diff --git a/docs/images/papers/QICKpapers_scaling_May26.png b/docs/source/images/papers/QICKpapers_scaling_May26.png similarity index 100% rename from docs/images/papers/QICKpapers_scaling_May26.png rename to docs/source/images/papers/QICKpapers_scaling_May26.png diff --git a/docs/images/papers/QICKpapers_scaling_Sep26.png b/docs/source/images/papers/QICKpapers_scaling_Sep26.png similarity index 100% rename from docs/images/papers/QICKpapers_scaling_Sep26.png rename to docs/source/images/papers/QICKpapers_scaling_Sep26.png diff --git a/docs/images/papers/QICKpapers_scaling_pie_May26.png b/docs/source/images/papers/QICKpapers_scaling_pie_May26.png similarity index 100% rename from docs/images/papers/QICKpapers_scaling_pie_May26.png rename to docs/source/images/papers/QICKpapers_scaling_pie_May26.png diff --git a/docs/images/quick_start/Bootmodeswitch.png b/docs/source/images/quick_start/Bootmodeswitch.png similarity index 100% rename from docs/images/quick_start/Bootmodeswitch.png rename to docs/source/images/quick_start/Bootmodeswitch.png diff --git a/docs/images/quick_start/Eafterwrite.PNG b/docs/source/images/quick_start/Eafterwrite.PNG similarity index 100% rename from docs/images/quick_start/Eafterwrite.PNG rename to docs/source/images/quick_start/Eafterwrite.PNG diff --git a/docs/images/quick_start/boardpic_cartoon.PNG b/docs/source/images/quick_start/boardpic_cartoon.PNG similarity index 100% rename from docs/images/quick_start/boardpic_cartoon.PNG rename to docs/source/images/quick_start/boardpic_cartoon.PNG diff --git a/docs/images/quick_start/ciscorouter.PNG b/docs/source/images/quick_start/ciscorouter.PNG similarity index 100% rename from docs/images/quick_start/ciscorouter.PNG rename to docs/source/images/quick_start/ciscorouter.PNG diff --git a/docs/images/quick_start/correctpynqversion.PNG b/docs/source/images/quick_start/correctpynqversion.PNG similarity index 100% rename from docs/images/quick_start/correctpynqversion.PNG rename to docs/source/images/quick_start/correctpynqversion.PNG diff --git a/docs/images/quick_start/jupyternotebook1.PNG b/docs/source/images/quick_start/jupyternotebook1.PNG similarity index 100% rename from docs/images/quick_start/jupyternotebook1.PNG rename to docs/source/images/quick_start/jupyternotebook1.PNG diff --git a/docs/images/quick_start/jupyternotebook2.PNG b/docs/source/images/quick_start/jupyternotebook2.PNG similarity index 100% rename from docs/images/quick_start/jupyternotebook2.PNG rename to docs/source/images/quick_start/jupyternotebook2.PNG diff --git a/docs/images/quick_start/largeimagefile.PNG b/docs/source/images/quick_start/largeimagefile.PNG similarity index 100% rename from docs/images/quick_start/largeimagefile.PNG rename to docs/source/images/quick_start/largeimagefile.PNG diff --git a/docs/images/quick_start/port_forward.png b/docs/source/images/quick_start/port_forward.png similarity index 100% rename from docs/images/quick_start/port_forward.png rename to docs/source/images/quick_start/port_forward.png diff --git a/docs/images/quick_start/pscpfolderstructure.PNG b/docs/source/images/quick_start/pscpfolderstructure.PNG similarity index 100% rename from docs/images/quick_start/pscpfolderstructure.PNG rename to docs/source/images/quick_start/pscpfolderstructure.PNG diff --git a/docs/images/quick_start/pullingdataofftheboard.PNG b/docs/source/images/quick_start/pullingdataofftheboard.PNG similarity index 100% rename from docs/images/quick_start/pullingdataofftheboard.PNG rename to docs/source/images/quick_start/pullingdataofftheboard.PNG diff --git a/docs/images/quick_start/pushingdatatotheboard.PNG b/docs/source/images/quick_start/pushingdatatotheboard.PNG similarity index 100% rename from docs/images/quick_start/pushingdatatotheboard.PNG rename to docs/source/images/quick_start/pushingdatatotheboard.PNG diff --git a/docs/images/quick_start/putty1.PNG b/docs/source/images/quick_start/putty1.PNG similarity index 100% rename from docs/images/quick_start/putty1.PNG rename to docs/source/images/quick_start/putty1.PNG diff --git a/docs/images/quick_start/putty2.PNG b/docs/source/images/quick_start/putty2.PNG similarity index 100% rename from docs/images/quick_start/putty2.PNG rename to docs/source/images/quick_start/putty2.PNG diff --git a/docs/images/quick_start/putty3.PNG b/docs/source/images/quick_start/putty3.PNG similarity index 100% rename from docs/images/quick_start/putty3.PNG rename to docs/source/images/quick_start/putty3.PNG diff --git a/docs/images/quick_start/pynqstartup.PNG b/docs/source/images/quick_start/pynqstartup.PNG similarity index 100% rename from docs/images/quick_start/pynqstartup.PNG rename to docs/source/images/quick_start/pynqstartup.PNG diff --git a/docs/images/quick_start/pynqversion.PNG b/docs/source/images/quick_start/pynqversion.PNG similarity index 100% rename from docs/images/quick_start/pynqversion.PNG rename to docs/source/images/quick_start/pynqversion.PNG diff --git a/docs/images/quick_start/static_ip.png b/docs/source/images/quick_start/static_ip.png similarity index 100% rename from docs/images/quick_start/static_ip.png rename to docs/source/images/quick_start/static_ip.png diff --git a/docs/images/quick_start/writetoEdrive.PNG b/docs/source/images/quick_start/writetoEdrive.PNG similarity index 100% rename from docs/images/quick_start/writetoEdrive.PNG rename to docs/source/images/quick_start/writetoEdrive.PNG diff --git a/docs/source/images/topics/fig1_xcom_block_diag.png b/docs/source/images/topics/fig1_xcom_block_diag.png new file mode 100644 index 000000000..be554705e Binary files /dev/null and b/docs/source/images/topics/fig1_xcom_block_diag.png differ diff --git a/docs/source/images/topics/fig3_fanout_box.png b/docs/source/images/topics/fig3_fanout_box.png new file mode 100644 index 000000000..135ea468d Binary files /dev/null and b/docs/source/images/topics/fig3_fanout_box.png differ diff --git a/docs/source/images/topics/fig4_xcom_sync.png b/docs/source/images/topics/fig4_xcom_sync.png new file mode 100644 index 000000000..1ef5b5f7a Binary files /dev/null and b/docs/source/images/topics/fig4_xcom_sync.png differ diff --git a/docs/source/images/topics/xcom_architecture.png b/docs/source/images/topics/xcom_architecture.png new file mode 100644 index 000000000..553393190 Binary files /dev/null and b/docs/source/images/topics/xcom_architecture.png differ diff --git a/docs/source/images/topics/xcom_cfg_if.png b/docs/source/images/topics/xcom_cfg_if.png new file mode 100644 index 000000000..33b811d88 Binary files /dev/null and b/docs/source/images/topics/xcom_cfg_if.png differ diff --git a/docs/source/images/topics/xcom_if.png b/docs/source/images/topics/xcom_if.png new file mode 100644 index 000000000..39805fe32 Binary files /dev/null and b/docs/source/images/topics/xcom_if.png differ diff --git a/docs/source/images/topics/xcom_new_architecture.png b/docs/source/images/topics/xcom_new_architecture.png new file mode 100644 index 000000000..54806ad62 Binary files /dev/null and b/docs/source/images/topics/xcom_new_architecture.png differ diff --git a/docs/source/images/topics/xcom_periph_timing.png b/docs/source/images/topics/xcom_periph_timing.png new file mode 100644 index 000000000..da29f67dc Binary files /dev/null and b/docs/source/images/topics/xcom_periph_timing.png differ diff --git a/docs/source/images/topics/xcom_reg_timing.png b/docs/source/images/topics/xcom_reg_timing.png new file mode 100644 index 000000000..89313c6c2 Binary files /dev/null and b/docs/source/images/topics/xcom_reg_timing.png differ diff --git a/docs/source/images/topics/xcom_registers.png b/docs/source/images/topics/xcom_registers.png new file mode 100644 index 000000000..e1f34604f Binary files /dev/null and b/docs/source/images/topics/xcom_registers.png differ diff --git a/docs/source/images/topics/xcom_waves.png b/docs/source/images/topics/xcom_waves.png new file mode 100644 index 000000000..b272e7c20 Binary files /dev/null and b/docs/source/images/topics/xcom_waves.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 000000000..9ddbd58bd --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,141 @@ +.. Sphinx Apidoc Tutorial documentation master file + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to the QICK documentation! +================================================= + +.. figure:: ../../graphics/QICK.jpg + :width: 100% + :align: center + +.. figure:: ../../graphics/ZCU216Board.jpg + :width: 100% + :align: center + +What is QICK? +============== + +The **Quantum Instrumentation Control Kit (QICK)** is an AMD RFSoC-based qubit controller +that supports the direct synthesis of control and readout pulses for quantum computing experiments. + +QICK consists of: + +- **Hardware**: AMD RFSoC board (ZCU111, ZCU216, or RFSoC4x2) with optional custom analog front-end +- **Firmware**: Custom FPGA logic including tProcessor, DDS generators, and readout system +- **Software**: Python library for pulse programming, data acquisition, and real-time feedback + +All schematics, firmware, and software are **open-source** and available on GitHub. + +Key Features +------------- + +- **Real-time control** with tProcessor (multi-core, < 10 ns timing resolution) +- **Direct RF synthesis** up to 6 GHz (ZCU216) without external mixers +- **Multi-tone generation** with phase-coherent muxed generators +- **Real-time feedback** and conditional logic +- **High-speed data streaming** and on-FPGA processing +- **Custom firmware** support via AXI-lite interface + +Quick Links +============ + +- **Source Code**: `GitHub Repository `_ +- **Firmware**: `qick/firmware `_ +- **QICK Paper**: `arXiv:2110.00557 `_ +- **Community**: :repofile:`Contact & Support ` + +📖 Documentation Navigation +============================ + +The documentation is organized as a **progressive learning path** from beginner to expert. + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: Getting Started + + quick_start + tutorials/README + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: Technical References + + modules + topics/index + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: For Experts + + firmware/index + tprocv1 + beta + alpha + +.. toctree:: + :maxdepth: 2 + :hidden: + :caption: Community + + contact + contributions + papers + +.. TODO: too verbose for front page, should move to firmware page (also the prose reeks of LLM) + The **firmware overview** is the map: which boards QICK supports, which + cores live on each one, and how the tProcessor, signal generators, and + readouts fit together on the FPGA -- read this before diving into any + specific core's page or the assembly-level tProcessor reference. + + The **readout system** turns ADC samples into the I/Q data your program gets + back. QICK offers several down-converter and buffer cores with different + tradeoffs (software- vs. tProc-configured, on-chip BRAM vs. DDR4 capture, + plus a resonator simulator for hardware-in-the-loop testing) -- start at + :doc:`firmware/readouts/index` for the concepts and the normal ``declare_readout()``/ + ``acquire()`` workflow, then dip into a specific core's page for register- + level detail. + + The **signal generators** turn your program's pulse definitions into DAC + output. QICK offers both single-tone arbitrary-envelope generators (for + shaped pulses) and multiplexed fixed-tone generators (for playing several + simultaneous tones from one channel) -- start at :doc:`firmware/generators/index` for an + overview of which core fits which experiment, then see :doc:`firmware/generators/sg_v6` for the + full worked Python examples shared across the family. + + The **support & utility cores** are smaller IP blocks -- a constant-IQ tone + source, a tProc-output register/trigger helper, and an AXI-Stream buffering + core -- that don't fit into either the readout or signal-generator families + above; see :doc:`firmware/support_cores` for what each one does and how (or whether) + it's exposed to Python. + + The **tProcessor v2 reference manual** is assembly-level detail -- + instruction encodings, register bit-fields, timing model -- for when you're + writing or debugging tProc assembly directly, not something you need to read + top-to-bottom to use QICK day to day. + + **tProc v1** is the previous-generation tProcessor, superseded by v2 on all + current firmware. Only relevant if you're maintaining pre-v2 code or + firmware. + +**New to QICK?** Start here: + +1. Read the :doc:`quick_start` guide to set up your board +2. Complete the :doc:`tutorials/README` to learn core concepts and apply them in measurements +3. Read :doc:`firmware/index` for a big-picture view of the QICK firmware + +**Already familiar with QICK?** Jump directly to: + +- :doc:`firmware/readouts/index` and :doc:`firmware/generators/index` for the readout/generator firmware + reference, organized by which core is on your channel +- :doc:`modules` for full API documentation +- :doc:`topics/index` for deep dives on specific topics +- :doc:`tprocv2_trm` for tProcessor instruction reference + +Academic Papers +================ + +For a list of academic papers published using the QICK system, see :doc:`papers`. diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 000000000..771d35cd5 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,99 @@ +Python API +========== + +This page is the raw, auto-generated API reference -- every public class, +method, and function in ``qick_lib``, with its docstring. For most tasks +you don't need to browse it directly: the narrative pages +(:doc:`/firmware/generators/index`, :doc:`/firmware/readouts/index`, :doc:`/tprocv2_trm`, ...) already +link to the specific method you need via cross-references. Come here when +you need the full signature/parameter list for something, or want to see +everything a class exposes at a glance. + +The modules are grouped below by what you'd actually reach for them for, +not alphabetically. + +Writing programs +----------------- + +The classes you subclass and call directly when writing an experiment. +:mod:`qick.asm_v2` is the current tProc v2 API (used throughout this doc +set); :mod:`qick.asm_v1`/:mod:`qick.averager_program` are the legacy tProc +v1 equivalent (see :doc:`/tprocv1`). :mod:`qick.qick_asm` holds the base +classes and methods (``declare_gen``, ``declare_readout``, ``add_pulse``, +``add_envelope``, ...) shared by both. + +.. autosummary:: + :toctree: _autosummary + + qick.qick_asm + qick.asm_v2 + qick.asm_v1 + qick.averager_program + +Connecting to the board +------------------------- + +.. autosummary:: + :toctree: _autosummary + + qick + qick.qick + qick.rfboard + qick.pyro + +:class:`.QickSoc` (:mod:`qick.qick`) is the main entry point -- instantiate +it once, then reach every generator/readout/buffer/tProc driver through its +``soc.gens``/``soc.readouts``/``soc.avg_bufs``/``soc.tproc`` arrays (see +:doc:`/firmware/index` for what's on your board). :mod:`qick.rfboard` extends it +for boards with an external RF front-end. :mod:`qick.pyro` lets you connect +to a board running headless over the network instead of locally. + +Hardware drivers +------------------- + +You'll interact with these through the ``soc.gens``/``soc.readouts``/ +``soc.avg_bufs``/``soc.tproc`` arrays above, not by importing these +modules directly -- see :doc:`/firmware/generators/index`, :doc:`/firmware/readouts/index`, and +:doc:`/tprocv1`/:doc:`/tprocv2_trm` for the driver classes' individual +pages (register maps, RTL cross-references) rather than the bare +docstrings here. + +.. autosummary:: + :toctree: _autosummary + + qick.drivers.generator + qick.drivers.readout + qick.drivers.tproc + +Assembly and compilation internals +------------------------------------- + +Normal programs don't call these directly -- :mod:`qick.asm_v2`/ +:mod:`qick.asm_v1` invoke them for you. Useful if you're debugging a +compiled program or working with raw assembly text (see +:doc:`/topics/asmv2_cheatsheet`). + +.. autosummary:: + :toctree: _autosummary + + qick.tprocv2_assembler + qick.parser + +Utilities and framework internals +------------------------------------- + +.. autosummary:: + :toctree: _autosummary + + qick.helpers + qick.streamer + qick.ip + +:mod:`qick.helpers` holds small standalone support functions. +:mod:`qick.streamer` is :class:`.DataStreamer`, a polling thread for +continuous acquisition -- not a driver for a specific IP core (see +:doc:`/firmware/support_cores` if you were looking for the ``axis_streamer`` RTL +block instead, a different thing despite the name). :mod:`qick.ip` has the +base :class:`.SocIP`/:class:`.QickIP` driver classes and the board metadata +tracing every driver's ``configure_connections()`` uses -- framework +internals, only relevant if you're writing a new driver. diff --git a/docs/papers.rst b/docs/source/papers.rst similarity index 100% rename from docs/papers.rst rename to docs/source/papers.rst diff --git a/docs/quick_start.rst b/docs/source/quick_start.rst similarity index 100% rename from docs/quick_start.rst rename to docs/source/quick_start.rst diff --git a/docs/source/topics/XCOM-commands.rst b/docs/source/topics/XCOM-commands.rst new file mode 100644 index 000000000..3b2318b37 --- /dev/null +++ b/docs/source/topics/XCOM-commands.rst @@ -0,0 +1,550 @@ +.. _xcom-commands: + +XCOM Commands Reference +======================= + +This document provides a complete reference for all XCOM commands. Commands are sent either: + +- **Over the network (NET)**: To other boards (OP code MSB = 0) +- **Locally (LOC)**: To the local XCOM instance (OP code MSB = 1) + +NET Command Message Format +-------------------------- + +NET messages have a header followed by data: + +.. list-table:: NET Message Structure + :header-rows: 1 + :widths: 30 30 40 + + * - Field + - Bits + - Description + * - OP + - HEADER[7:4] + - Operation code (4 bits) + * - ADDR + - HEADER[3:0] + - Destination board address + * - DATA + - Variable + - Payload (8, 16, or 32 bits) + +If ``ADDR = 0``, the command is broadcast to all boards in the network. + +LOC Command Message Format +-------------------------- + +LOC commands are written directly to the ``XCOM_CTRL`` register: + +- ``OP_I`` (``ctrl[5:1]``) contains the command +- ``EN`` (``ctrl[0]``) triggers execution (auto-resets after one cycle) + +NET Commands +------------ + +XCOM_CLEAR_FLAG +~~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_CLEAR_FLAG + - 0 + - 00000 + - ADDR + - — + +**Description**: +This is a NET command. Clear the `qp_flag` pin in the external peripheral interface of the QICK processor. This signal can be used to inform the QICK processor some state in the execution of a program between boards. The DT1 register contains the address of the board where to execute the command. + +If ADDR = 0 that means it is a broadcast message and all the boards in the network will execute this command. + +--- + +XCOM_SET_FLAG +~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_SET_FLAG + - 1 + - 00001 + - ADDR + - — + +**Description**: + This is a NET command. Set the `qp_flag` pin in the external peripheral interface of the QICK processor. This signal can be used to + inform the QICK processor some state in the execution of a program between boards. + The DT1 register contains the address of the board where to execute the command. + +If ADDR = 0 that means it is a broadcast message and all the boards in the network will execute this command. + +Sets the ``qp_flag`` pin on the specified remote board. + +--- + +XCOM_SEND_8BIT +~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_SEND_8BIT_1 + - 2 + - 00010 + - ADDR + - DATA[7:0] + * - XCOM_SEND_8BIT_2 + - 3 + - 00011 + - ADDR + - DATA[7:0] + +**Description**: +These are NET commands. Sends a byte of data to the board with address ADDR. DT1 +register selects where to write the data in DT2 register. + +* With `XCOM_SEND_8BIT_1` the data contained in DT2 is written into the `qp_dt1` + and into the `o_time_update_data` outputs. This command writes to the registers + in the CORE processor of the receiving (ADDR) board. + +* With `XCOM_SEND_8BIT_2` the data contained in DT2 is written into the 16x32 + memory of the receiving (ADDR) board. + +If ADDR = 0 that means it is a broadcast message and all the boards in the +network will execute this command. + +--- + +XCOM_SEND_16BIT +~~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_SEND_16BIT_1 + - 4 + - 00100 + - ADDR + - DATA[15:0] + * - XCOM_SEND_16BIT_2 + - 5 + - 00101 + - ADDR + - DATA[15:0] + +**Description**: +These are NET commands. Sends 16-bit data to the board with address ADDR. DT1 +register selects where to write the data in DT2 register. + +* With `XCOM_SEND_16BIT_1` the data contained in DT2 is written into the `qp_dt1` + and into the `o_time_update_data` outputs. This command writes to the registers + in the CORE processor of the receiving (ADDR) board. + +* With `XCOM_SEND_16BIT_2` the data contained in DT2 is written into the 16x32 + memory of the receiving (ADDR) board. + +If ADDR = 0 that means it is a broadcast message and all the boards in the +network will execute this command. + +--- + +XCOM_SEND_32BIT +~~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_SEND_32BIT_1 + - 6 + - 00110 + - ADDR + - DATA[31:0] + * - XCOM_SEND_32BIT_2 + - 7 + - 00111 + - ADDR + - DATA[31:0] + +**Description**: +These are NET commands. Sends 32-bit data to the board with address ADDR. DT1 +register selects where to write the data in DT2 register. + +* With `XCOM_SEND_32BIT_1` the data contained in DT2 is written into the `qp_dt1` + and into the `o_time_update_data` outputs. This command writes to the registers + in the CORE processor of the receiving (ADDR) board. + +* With `XCOM_SEND_32BIT_2` the data contained in DT2 is written into the 16x32 + memory of the receiving (ADDR) board. + +If ADDR = 0 that means it is a broadcast message and all the boards in the +network will execute this command. + +--- + +XCOM_QRST_SYNC +~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_QRST_SYNC + - 8 + - 01000 + - ADDR + - — + +**Description**: +This is a NET command. DT1 register selects the address of the board to which +the local board wants to synchronize to. The transmitter board waits for the +next rising edge in the PPS signal (synchronization signal) to send the +command and at the same time it will wait for the next rising edge of the +PPS signal to set the local `o_proc_start` signal. The receiving board, +once receive the `XCOM_QRST_SYNC` command will wait for the next rising +edge in the PPS signal and then will set the `o_proc_start` signal to +let the QICK processor start with its operations. The `o_proc_start` signal +will remain settled for 8 `t_clk` clock cycles and then goes to zero again. +In that way, all the connected boards will start its QICK processor +operations at the same time, that means 2 PPS after the `XCOM_QRST_SYNC` +command was issued by the initial (master/main) board. + +If ADDR = 0 that means it is a broadcast message and all the boards in +the network will execute this command. + +--- + +XCOM_AUTO_ID +~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_AUTO_ID + - 9 + - 01001 + - — + - — + +**Description**: +This is a NET command. The board receiving this command will auto +assign itself a board ID calculated as the ID of the transmitter +board plus one, so RX ID = TX ID + 1. + +--- + +XCOM_UPDATE_DT8 / 16 / 32 +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_UPDATE_DT8 + - 10 + - 01010 + - ADDR + - DATA + * - XCOM_UPDATE_DT16 + - 12 + - 01100 + - ADDR + - DATA + * - XCOM_UPDATE_DT32 + - 14 + - 01110 + - ADDR + - DATA + +**Description**: +This is a NET command and for now they are just placeholders for future +implementations. Not specific task assigned yet. + +--- + +XCOM_QCTRL +~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_QCTRL + - 11 + - 01011 + - ADDR + - CONTROL[31:0] + +**Description**: +This is a NET command. DT1 register selects the board where to write the data +in DT2 register. This command is related to the control of the QICK processor, +see details in :doc:`xcom`. + +It depends on what DT2[2:0] contains to select what to do in the receiving board: + +* DT2[2:0] = 0, N/A. +* DT2[2:0] = 1, N/A. +* DT2[2:0] = 2, the `o_time_rst` signal is set for 8 `t_clk` clock cycles and then goes to zero again. +* DT2[2:0] = 3, the `o_time_update` signal is set for 8 `t_clk` clock cycles and then goes to zero again. +* DT2[2:0] = 4, the `o_core_start` signal is set for 8 `t_clk` clock cycles and then goes to zero again. +* DT2[2:0] = 5, the `o_core_stop` signal is set for 8 `t_clk` clock cycles and then goes to zero again. +* DT2[2:0] = 6, the `o_proc_start` signal is set for 8 `t_clk` clock cycles and then goes to zero again. +* DT2[2:0] = 7, the `o_proc_stop` signal is set for 8 `t_clk` clock cycles and then goes to zero again. + +.. list-table:: Control Encoding + :header-rows: 1 + + * - DT2[2:0] + - Operation + - Description + * - 2 + - Time reset + - Reset time counter + * - 3 + - Time update + - Update time + * - 4 + - Core start + - Start core + * - 5 + - Core stop + - Stop core + * - 6 + - Proc start + - Start processor + * - 7 + - Proc stop + - Stop processor + +--- + +XCOM_RFU +~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_RFU_1 + - 13 + - 01101 + - — + - — + * - XCOM_RFU_2 + - 15 + - 01111 + - — + - — + +**Description**: + This are NET commands and for now they are just placeholders for future + implementations. Not specific task assigned yet. + +--- + +LOC Commands +------------ + +XCOM_SET_ID +~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_SET_ID + - 16 + - 10000 + - ID + - — + +**Description**: +This is a LOC command. Set the ID of the board locally. The DT1 register +contains the desired ID. The ID must be a number between 1 and 15. + +--- + +XCOM_WRITE_FLAG +~~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_WRITE_FLAG + - 17 + - 10001 + - FLAG + - — + +**Description**: +This is a LOC command. Set the `qp_flag` pin in the external peripheral interface +of the QICK processor. This signal can be used to inform the QICK processor some +state in the execution of a program between boards. The DT1[0] bit is used to +set (1) or unset (0) this flag. + +--- + +XCOM_WRITE_REG +~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_WRITE_REG + - 18 + - 10010 + - SEL + - DATA[31:0] + +**Description**: +This is a LOC command. DT1 register selects where to write the data in DT2 +register. If DT1[0] = 0, the data contained in DT2 is written into the `qp_dt1` +output. If DT1[0] = 1 the data contained in DT2 is written into the `qp_dt2` +output. This command writes to the registers in the CORE processor. When DT1[0] += 0 this command also writes the data in DT2 register into the +`o_time_update_data` output. + +--- + +XCOM_WRITE_MEM +~~~~~~~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_WRITE_MEM + - 19 + - 10011 + - ADDR + - DATA[31:0] + +**Description**: +This is a LOC command. DT1 register selects the address where to write the data +in DT2 register. Address range is 0 - 15. This command writes to the internal +16x32 memory. + +--- + +XCOM_RST +~~~~~~~~ + +.. list-table:: Command Format + :header-rows: 1 + + * - Command + - Code + - OP + - DT1 + - DT2 + * - XCOM_RST + - 31 + - 11111 + - — + - — + +**Description**: +This is a LOC command. It reset all the internal registers in the local `xcom` +instance. + +--- + +Python API Example +------------------ + +.. code-block:: python + + from qick.pyro import make_proxy + + # Connect to board + soc = make_proxy("board_ip_address") + + # Reset XCOM + soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_RST'], 0, 0) + + # Auto-assign IDs + soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_AUTO_ID'], 0, 0) + + # Set local ID to 2 + soc.xcom_0.set_local_id(2) + + # Send 32-bit data to board 5 + soc.xcom_0.send_word(0xDEADBEEF, dst=5, reg=2) + + # Read status + soc.xcom_0.print_status() + +See Also +-------- + +* :doc:`xcom` — XCOM overview and architecture + +* `XCOM Network Synchronization <../tutorials/14_XCOM_Network_Synchronization.ipynb>`_ — XCOM demonstration notebook + +* `XCOM paper (arXiv:2603.18977v1) `_ diff --git a/docs/source/topics/asmv2_cheatsheet.rst b/docs/source/topics/asmv2_cheatsheet.rst new file mode 100644 index 000000000..21500621a --- /dev/null +++ b/docs/source/topics/asmv2_cheatsheet.rst @@ -0,0 +1,264 @@ +tProc v2 ASM Cheatsheet +======================= + +This cheatsheet is for the **tProcessor v2** (72-bit instructions, 32-bit registers). +For the complete reference manual, see :doc:`/tprocv2_trm`. + +Quick Reference +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 40 60 + + * - Operation + - Assembly Code + * - Load immediate to register + - ``REG_WR r0 imm #100`` + * - Copy register to register + - ``REG_WR r1 op -op(r0)`` + * - Load from DMEM + - ``REG_WR r0 dmem [&10]`` + * - Store to DMEM + - ``DMEM_WR [&10] imm #42`` + * - Load waveform from WMEM + - ``REG_WR r_wave wmem [&5]`` + * - Store waveform to WMEM + - ``WMEM_WR [&5]`` + * - Write data port + - ``DPORT_WR p0 imm 1 @1000`` + * - Write trigger + - ``TRIG p0 set @150`` + * - Write wave port + - ``WPORT_WR p1 r_wave @1000`` + * - Read input port + - ``DPORT_RD p0`` + * - Conditional jump + - ``JUMP LABEL -if(NZ)`` + * - Call subroutine + - ``CALL MY_SUB`` + * - Return from subroutine + - ``RET`` + * - Wait for time + - ``WAIT time @1000`` + * - Wait for division + - ``WAIT div_dt`` + * - Wait for ARITH (no WAIT macro; poll manually) + - ``TEST -op(s_status AND bit_arith_new) -uf`` / ``JUMP HERE -if(Z)`` + * - Wait for input data + - ``WAIT port_dt`` + * - Start division + - ``DIV r1 r2`` + * - Start multiply (ARITH) + - ``ARITH T r1 r2`` + * - Multiply-accumulate + - ``ARITH PTP r1 r2 r3 r4`` ``; (r1+r2)*r3+r4`` + * - Set internal flag + - ``FLAG set`` + * - Clear internal flag + - ``FLAG clr`` + * - Reset time + - ``TIME rst`` + * - Increment reference time + - ``TIME inc_ref #100`` + * - Clear peripheral flag + - ``CLEAR arith`` + * - No operation + - ``NOP`` + +Condition Codes +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 40 40 + + * - Code + - Condition + - Use Case + * - ``-if(Z)`` + - Zero (result == 0) + - Check if counter reached zero + * - ``-if(NZ)`` + - Non-zero (result != 0) + - Loop while counter > 0 + * - ``-if(S)`` + - Negative (result < 0) + - Check threshold crossing + * - ``-if(NS)`` + - Non-negative (result >= 0) + - Opposite of S + * - ``-if(F)`` + - Flag set + - Wait for external trigger + * - ``-if(NF)`` + - Flag clear + - Wait for flag to clear + +Address Modes +------------- + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - Mode + - Syntax + - Example + * - Literal (DMEM) + - ``[&addr]`` + - ``DMEM_WR [&10] imm #5`` + * - Register (DMEM) + - ``[rX]`` + - ``DMEM_WR [r0] imm #5`` + * - Indexed literal (DMEM) + - ``[rX + &offset]`` + - ``DMEM_WR [r1+&4] imm #5`` + * - Indexed register (DMEM) + - ``[rX + rY]`` + - ``DMEM_WR [r2+r3] imm #5`` + * - Literal (WMEM) + - ``[&addr]`` + - ``WMEM_WR [&10]`` + * - Register (WMEM) + - ``[rX]`` + - ``WMEM_WR [r0]`` + +Python Examples +--------------- + +**Basic program loading:** + +.. code-block:: python + + import numpy as np + from qick import * + from qick.tprocv2_assembler import Assembler + + soc = QickSoc() + + asm_code = """ + REG_WR r0 imm #100 + DPORT_WR p0 reg r0 @1000 + .END + """ + + # 1. Assembly + _, bin_arr = Assembler.str_asm2bin(asm_code) + # 2. Convert to ndarray int32 (N,8) -- must be int32, the assembler + # packs each 72-bit instruction into three ints (two 32-bit + one 8-bit) + buff = np.array(bin_arr, dtype=np.int32) + # 3. Load in pmem + soc.tproc.load_mem('pmem', buff, check=False) + # 4. Execute + soc.tproc.start() + + +**Waveform loading and playback:** + +.. code-block:: python + + # Load waveform to WMEM using the correct method for tProc V2 + def pack_waveform(freq, phase, env, gain, length, conf): + """Pack waveform parameters into 8-word (256-bit) format for tProc WMEM""" + wave = np.zeros(8, dtype=np.int32) + wave[0] = freq & 0xFFFFFFFF # 32-bit frequency + wave[1] = phase & 0xFFFFFFFF # 32-bit phase + wave[2] = env & 0xFFFFFF # 24-bit envelope index (w_env) + wave[3] = gain & 0xFFFFFFFF # 32-bit gain + wave[4] = length & 0xFFFFFFFF # 32-bit length + wave[5] = conf & 0xFFFF # 16-bit config (w_conf) + return wave.reshape(1, 8) + + # Create and pack waveform + waveform_data = pack_waveform( + freq=100_000_000, # 100 MHz + phase=0, + env=0, + gain=32768, # 1.0 (max) + length=1024, + conf=0 + ) + + # Load waveform to WMEM (tProc V2 method) + soc.tproc.load_mem('wmem', waveform_data, addr=0, check=False) + + # Assembly program to play waveform (corrected syntax) + asm_code = """ + WAIT time @1000 ; Wait for 1000 clock cycles + WPORT_WR p0 wmem [&0] ; Play waveform directly from WMEM address 0 + .END + """ + + # Assemble and run + _, bin_arr = Assembler.str_asm2bin(asm_code) + buff = np.array(bin_arr, dtype=np.int32) + soc.tproc.load_mem('pmem', buff, check=False) + soc.tproc.start() + +**Feedback Example (read ADC, conditional output)** + +.. code-block:: python + + asm_feedback = """ + LOOP: + WAIT port_dt + DPORT_RD p0 + TEST -op(s_port_l - #32768) -uf + JUMP LOAD_HIGH -if(NS) + JUMP LOAD_LOW -if(S) + + LOAD_HIGH: + REG_WR r_wave wmem [&0] + WPORT_WR p0 r_wave @1000 + JUMP NEXT + + LOAD_LOW: + REG_WR r_wave wmem [&1] + WPORT_WR p0 r_wave @1000 + JUMP NEXT + + NEXT: + JUMP LOOP + + .END + """ + +**Optional, adding a counter (iterations)** + +.. code-block:: python + + asm_feedback_with_counter = """ + DMEM_WR [&0] imm #100 + + LOOP: + WAIT port_dt + DPORT_RD p0 + TEST -op(s_port_l - #32768) -uf + JUMP LOAD_HIGH -if(NS) + JUMP LOAD_LOW -if(S) + + LOAD_HIGH: + REG_WR r_wave wmem [&0] + WPORT_WR p0 r_wave @1000 + JUMP DEC + + LOAD_LOW: + REG_WR r_wave wmem [&1] + WPORT_WR p0 r_wave @1000 + JUMP DEC + + DEC: + DMEM_WR [&0] imm #99 + JUMP LOOP + + .END + """ + +Related Documentation +--------------------- + +* :doc:`/tprocv2_trm` - Complete tProcessor v2 reference manual +* :doc:`/firmware/index` - Firmware overview and channel assignments +* :doc:`/firmware/generators/sg_v6` - Signal Generator v6 documentation +* :doc:`/firmware/readouts/index` - Readout system documentation diff --git a/docs/topics/changing_fs.rst b/docs/source/topics/changing_fs.rst similarity index 100% rename from docs/topics/changing_fs.rst rename to docs/source/topics/changing_fs.rst diff --git a/docs/source/topics/freq_matching.rst b/docs/source/topics/freq_matching.rst new file mode 100644 index 000000000..9cb4b9bd9 --- /dev/null +++ b/docs/source/topics/freq_matching.rst @@ -0,0 +1,79 @@ +How to ensure frequency matching +================================ + +Generators (SG-v6) and readouts have different frequency units because they run on different clocks (DAC clock vs ADC clock, with different sampling rates). +For coherent measurements, the upconversion frequency (DAC) and downconversion frequency (ADC) must be **exactly equal**. +If they are not, the acquired data will show a sliding phase, making it impossible to get consistent results. + +Frequency Matching Methods +-------------------------- + +There are two ways to ensure frequency matching in QICK: + +**Method 1: Match during conversion (recommended)** + +When converting a frequency to an integer register value, specify both the channel you are configuring and the channel you want to frequency-match to: + +.. code-block:: python + + from qick import * + + soc = QickSoc() + soccfg = soc + + # Convert frequency for generator 0, matching to readout 0 (freq2reg takes MHz) + freq_reg = soccfg.freq2reg(100.0, gen_ch=0, ro_ch=0) + + # Or for readout, matching to generator 0 + freq_reg = soccfg.freq2reg_adc(100.0, ro_ch=0, gen_ch=0) + +**Method 2: Pre-round the frequency** + +Round the frequency to the closest value that is valid on both channels: + +.. code-block:: python + + # Get the nearest frequency (in MHz) that works for both gen_ch=0 and ro_ch=0 + matched_freq = soccfg.adcfreq(100.0, gen_ch=0, ro_ch=0) + freq_reg = soccfg.freq2reg(matched_freq, gen_ch=0) + +Trade-offs +---------- + +Frequency matching **reduces frequency resolution**, because the smallest step is now the least common multiple (LCM) of the two channels' frequency steps. + +- Typical resolution: ~10 Hz, which is sufficient for most qubit experiments +- To disable matching (if you need finer resolution), specify `None` as the other channel: + +.. code-block:: python + + # No matching - highest resolution + freq_reg = soccfg.freq2reg(100.0, gen_ch=0, ro_ch=None) + +Multiple Generators +------------------- + +If you have two generators that need to be phase-locked (e.g., for qubit drive and cavity drive), both should be frequency-matched to the same readout: + +.. code-block:: python + + # Match both generators to readout 0 + freq_reg_drive = soccfg.freq2reg(100.0, gen_ch=0, ro_ch=0) + freq_reg_cavity = soccfg.freq2reg(100.0, gen_ch=1, ro_ch=0) + +If they are matched to different readouts (or not matched), they will have slightly different frequencies and the phase between them will drift. + +Best Practices +-------------- + +- In most QICK firmwares, all generators and readouts have the same sampling frequency, so matching to channel 0 works for everything. +- Make it a habit to always specify the matched channel explicitly. +- Use `soccfg.freq2reg()` for generator channels and `soccfg.freq2reg_adc()` for readout channels, always passing the other channel to match against. + +Related Documentation +--------------------- + +* :doc:`/firmware/generators/sg_v6` - Signal Generator v6 +* :doc:`/firmware/readouts/index` - Readout system +* :doc:`/firmware/index` - Clock domains and sample rates +* :doc:`changing_fs` - Custom sample rates diff --git a/docs/topics/gen_config.rst b/docs/source/topics/gen_config.rst similarity index 100% rename from docs/topics/gen_config.rst rename to docs/source/topics/gen_config.rst diff --git a/docs/source/topics/index.rst b/docs/source/topics/index.rst new file mode 100644 index 000000000..9f4502a28 --- /dev/null +++ b/docs/source/topics/index.rst @@ -0,0 +1,92 @@ +================ +Technical Topics +================ + +This section contains detailed technical reference material for advanced users. + +These topics assume you have completed the basic tutorials (00-05) and are familiar with: +- QICK program structure (`_initialize`, `_body`) +- Pulse definition and sequencing +- Basic readout configuration + +.. toctree:: + :maxdepth: 1 + :caption: Core Concepts + + freq_matching + timing + playing_pulses + gen_config + readout_modes + units + +.. toctree:: + :maxdepth: 1 + :caption: Advanced Topics + + pyro4 + changing_fs + reference_clock + +Topic Descriptions +================== + +**freq_matching** + How frequency matching works between generators and readouts in tProc v2 vs. v1. + +**timing** + Detailed explanation of timeline management, delays, and synchronization. + +**playing_pulses** + How to define and play pulses with different styles (const, arb, flat_top). + +**gen_config** + Complete reference for generator configuration parameters. + +**readout_modes** + Accumulated (``acquire``) vs. decimated (``acquire_decimated``) readout: what + each mode returns, when to use which, and their shared setup. + +**units** + Understanding QICK's time, frequency, and amplitude units. + +**reference_clock** + Configuring external reference clocks and clock distribution. + +**changing_fs** + How to change the FPGA sampling frequency for different bandwidths. + +**asmv2_cheatsheet** + Quick reference for tProc v2 assembly instructions. + +**tutorials** + Link to the full Jupyter notebook tutorials. + +**tprocv2_migration** + Short migration guide for tProc v1 users: hardware sweeps, timing calls, pulse playback, and units. + +**example_code** + Larger example codebases shared by QICK collaborators, for both tProc v1 and v2. + +**xcom** + XCOM: Full mesh network for multi-board synchronization and low-latency communication. Covers hardware requirements, communication protocol, command set, and Python interface. + +**XCOM-commands** + Complete reference of all XCOM commands, including NET commands (for network communication) and LOC commands (for local control). + +**pyro4** + Pyro4: Network-based multi-board connection for distributed control and data acquisition. + +When to Use Each Topic +====================== + +- **New to timing concepts?** Start with :doc:`timing` +- **Confused about frequencies?** Read :doc:`freq_matching` +- **Need to shape pulses?** See :doc:`playing_pulses` +- **Changing hardware config?** Check :doc:`gen_config` and :doc:`changing_fs` +- **Not sure whether to use `acquire` or `acquire_decimated`?** See :doc:`readout_modes` +- **Using external clock?** Refer to :doc:`reference_clock` +- **Writing assembly code?** Keep :doc:`asmv2_cheatsheet` handy +- **Coming from tProc v1?** Start with :doc:`tprocv2_migration` +- **Synchronizing multiple boards?** Read :doc:`xcom` for the network-based solution +- **Need XCOM command details?** See :doc:`XCOM-commands` for the complete command reference diff --git a/docs/source/topics/playing_pulses.rst b/docs/source/topics/playing_pulses.rst new file mode 100644 index 000000000..83b441885 --- /dev/null +++ b/docs/source/topics/playing_pulses.rst @@ -0,0 +1,148 @@ +How to play pulses with QICK +============================ + +.. note:: + Earlier revisions of this page built every example around raw + ``WPORT_WR`` assembly, a fictional ``tproc.load_wave()``/ + ``gen.load_waveform()``/``gen.set_pulse()`` API, and claimed + :meth:`.QickProgram.pulse` "is designed for the older tProc v1 API" -- + the opposite is true: :meth:`.QickProgram.pulse` (specifically + :meth:`.QickProgramV2.pulse`) *is* the current, recommended way to play a + pulse on tProc v2, and every other page in this doc set uses it. This + page was rewritten to match, verified against ``qick.qick_asm``/ + ``qick.asm_v2`` in the same session that found the problem. + +With the tProcessor v2 and Signal Generator v6 (or any of QICK's other +generator cores -- see :doc:`/firmware/generators/index`), playing a pulse involves three +steps, all done through a :class:`.QickProgram` subclass: + +.. contents:: + :local: + :depth: 2 + +Overview +-------- + +1. **Declare the generator** (once, in ``_initialize()``) -- see + :meth:`.QickProgram.declare_gen`. +2. **Load an envelope** (optional -- skip for rectangular "const" pulses) + and **define the pulse**, giving it a name -- see + :meth:`.QickProgram.add_envelope` and :meth:`.QickProgram.add_pulse`. +3. **Play it by name at a chosen time** -- see :meth:`.QickProgram.pulse`. + +Step 1: Loading a waveform envelope +------------------------------------ + +(You skip this step for rectangular "const" pulses, which have no envelope.) + +Each generator with envelope memory (SG-v6, SG-v4, SG-int4-v2 -- not the +multiplexed SG-Mux8/SG-MixMux8 cores, which play fixed tones instead; see +:doc:`/firmware/generators/index`) stores I/Q envelope data internally. Multiple envelopes +can be stored per generator, and one envelope can be reused across several +pulses (e.g. a Gaussian shape reused for pulses at different frequencies). + +Upload envelope samples with :meth:`.QickProgram.add_envelope`, or use one +of the built-in shape generators that call it for you: + +.. code-block:: python + + from qick import * + + class MyProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_gen(ch=0, nqz=1) + + # Option A: build the envelope yourself and register it + import numpy as np + 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="my_gauss", idata=idata.astype(np.int16)) + + # Option B: use a built-in shape helper (equivalent to the above) + self.add_gauss(ch=0, name="my_gauss2", sigma=0.05, length=0.3) + +**Built-in envelope shape helpers** (all call ``add_envelope`` internally): + +- :meth:`.AbsQickProgram.add_gauss` - Gaussian pulse +- :meth:`.AbsQickProgram.add_triangle` - Triangular pulse +- :meth:`.AbsQickProgram.add_cosine` - Raised-cosine pulse +- :meth:`.AbsQickProgram.add_DRAG` - DRAG pulse for qubit control +- :meth:`.AbsQickProgram.add_envelope` - Custom arbitrary waveform (used above) + +Step 2: Defining the pulse +--------------------------- + +Pulse parameters (frequency, phase, gain, length, envelope, output style) +are set with :meth:`.QickProgram.add_pulse`, which registers a named pulse +you'll play later. The available parameters depend on ``style``: + +.. code-block:: python + + # A rectangular pulse -- no envelope needed + self.add_pulse(ch=0, name="square", style="const", + freq=100.0, phase=0, gain=0.5, length=1.0) # MHz, deg, -1..1, us + + # A shaped pulse, using an envelope registered in Step 1 + self.add_pulse(ch=0, name="shaped", style="arb", + freq=100.0, phase=0, gain=0.9, envelope="my_gauss") + + # A flat-top pulse: an envelope for the rise/fall, flat in between + self.add_pulse(ch=0, name="flattop", style="flat_top", + freq=100.0, phase=0, gain=0.7, length=0.5, envelope="my_gauss") + +**Dynamic parameter sweeps** (e.g. frequency) don't need a hand-written +loop that rewrites registers each iteration -- pass a :func:`.QickSweep1D` +as the parameter value and use :meth:`.QickProgram.add_loop`: + +.. code-block:: python + + self.add_loop("freq_loop", 100) + self.add_pulse(ch=0, name="chirp", style="const", + freq=QickSweep1D("freq_loop", 50.0, 150.0), + phase=0, gain=0.5, length=0.1) + +See :doc:`/tprocv2_trm`'s Chapter 13 for a complete sweep example, and +:doc:`/tprocv2_trm` for what ``add_pulse``/``pulse`` compile down to under +the hood (the 168-bit ``r_wave`` bus and ``WPORT_WR`` instruction), if you +need that level of detail. + +Step 3: Firing the pulse +------------------------- + +:meth:`.QickProgram.pulse` schedules a named pulse (defined in Step 2) to +play at a given time, relative to the current shot: + +.. code-block:: python + + def _body(self, cfg): + self.pulse(ch=0, name="square", t=0) + +**Triggering a readout at the same time as a pulse** -- pass both to the +same or a separate :meth:`.QickProgramV2.trigger` call at the matching time +(see :doc:`/firmware/readouts/index`'s Python Interface section for the full +``declare_readout``/``trigger``/``acquire`` workflow): + +.. code-block:: python + + def _body(self, cfg): + self.trigger(ros=[0], t=0) + self.pulse(ch=0, name="square", t=0) + +**Running the program** (this is what actually drives +``tproc.time_reset()``/``tproc.start()`` for you): + +.. code-block:: python + + prog = MyProgram(soccfg, reps=1000, final_delay=10.0, cfg={}) + iq = prog.acquire(soc) + +Related Documentation +--------------------- + +* :doc:`/tprocv2_trm` - tProcessor v2 instruction set and register-level detail +* :doc:`/firmware/generators/index` - Which generator cores support envelopes vs. fixed tones +* :doc:`/firmware/generators/sg_v6` - Signal Generator v6 documentation, with more worked examples +* :doc:`/firmware/readouts/index` - Declaring and triggering a readout alongside a pulse +* :doc:`/firmware/index` - Channel assignments and firmware overview +* :doc:`/tutorials/README` - tProc v2 tutorial examples diff --git a/docs/source/topics/pyro4.rst b/docs/source/topics/pyro4.rst new file mode 100644 index 000000000..287723a66 --- /dev/null +++ b/docs/source/topics/pyro4.rst @@ -0,0 +1,173 @@ +.. _pyro4: + +Pyro4: Multi-Board Connection and Distributed Control +====================================================== + +Pyro4 (Python Remote Objects) enables communication between multiple QICK boards over a network. This is essential for: + +- Running synchronized experiments across multiple RFSoC boards +- Distributing control and readout tasks +- Building scalable quantum control systems + +.. note:: + Pyro4 is different from XCOM. Pyro4 operates at the software/network level (Ethernet), while XCOM is a hardware-level low-latency solution using LVDS signals. Use Pyro4 for flexibility and ease of use; use XCOM for sub-100 ps synchronization and deterministic latency below 200 ns. + +Architecture +------------ + +The Pyro4 setup consists of three components: + +1. **Name Server**: A central registry that keeps track of all available QICK boards +2. **Server**: Runs on each QICK board, exposing its functionality via Pyro +3. **Client**: Your control PC or Jupyter notebook that connects to the name server and calls methods on remote boards + +:: + + ┌─────────────────────────────────────────────────────────────────────────────┐ + │ Pyro4 Architecture │ + ├─────────────────────────────────────────────────────────────────────────────┤ + │ │ + │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ │ QICK #1 │ │ QICK #2 │ │ Client │ │ + │ │ (Server) │ │ (Server) │ │ (Python) │ │ + │ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ + │ │ │ │ │ + │ └───────────────────┼───────────────────┘ │ + │ │ │ + │ ┌──────▼──────┐ │ + │ │ Name Server │ │ + │ │ (Pyro NS) │ │ + │ └─────────────┘ │ + │ │ + └─────────────────────────────────────────────────────────────────────────────┘ + +Setup Instructions +------------------ + +Install Pyro4 on all boards and the client machine: + +.. code-block:: bash + + pip install Pyro4 + +Start the name server on a machine accessible by all boards (e.g., your control PC): + +.. code-block:: bash + + python -m Pyro4.naming -n 0.0.0.0 -p 8888 + +Alternatively, use the script from the QICK repository: + +.. code-block:: bash + + wget https://raw.githubusercontent.com/openquantumhardware/qick/main/pyro4/nameserver.sh + chmod +x nameserver.sh + ./nameserver.sh + +On each QICK board, start the Pyro server: + +.. code-block:: python + + from qick.pyro import start_server + + start_server( + ns_host="your_nameserver_host", # IP of the machine running the name server + ns_port=8888, + proxy_name="qick1", # Unique name for this board + external_clk=True, # Optional: use external reference clock + adc_sample_rates={0:3072.0} # Optional: set ADC sample rates + ) + +For production deployments, use the systemd service files from the QICK repository: + +.. code-block:: bash + + wget https://raw.githubusercontent.com/openquantumhardware/qick/main/pyro4/qick_pyro.service + wget https://raw.githubusercontent.com/openquantumhardware/qick/main/pyro4/pyro_service.py + + sudo cp qick_pyro.service /etc/systemd/system/ + sudo systemctl daemon-reload + sudo systemctl enable qick_pyro + sudo systemctl start qick_pyro + +Client Usage +------------ + +In your Jupyter notebook or Python script, connect to the name server and create proxies to each board: + +.. code-block:: python + + from qick.pyro import make_proxy + + # Connect to boards + proxies = [] + for name in ['qick1', 'qick2', 'qick3']: + proxies.append(make_proxy(ns_host="your_nameserver_host", ns_port=8888, proxy_name=name)) + + # Each proxy behaves like a local QickSoc object + for soc, soccfg in proxies: + print(soccfg) + soc.reset_gens() + +Example: Multi-Board Synchronized Acquisition +--------------------------------------------- + +For a complete example, refer to the Jupyter notebooks in the `pyro4` directory of the QICK repository: +`02_client.ipynb `_. + +.. code-block:: python + + import asyncio + from qick.pyro import make_proxy + from qick.asm_v2 import AveragerProgramV2 + + # Connect to boards + proxies = [make_proxy(ns_host="localhost", ns_port=8888, proxy_name=f"qick{i}") for i in range(1, 3)] + + # Define a simple program + class SyncProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_gen(ch=cfg['gen_ch'], nqz=1) + self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len']) + self.add_pulse(ch=cfg['gen_ch'], name="pulse", + style="const", freq=cfg['freq'], + length=cfg['pulse_len'], gain=cfg['gain']) + + def _body(self, cfg): + self.pulse(ch=cfg['gen_ch'], name="pulse", t=0) + self.trigger(ros=[cfg['ro_ch']], t=cfg['trig_time']) + + # Run on each board + results = [] + for soc, soccfg in proxies: + prog = SyncProgram(soccfg, cfg=config) + results.append(prog.acquire(soc)) + + # Process results + for i, iq in enumerate(results): + print(f"Board {i+1} data shape: {iq.shape}") + +Asynchronous Multi-Board Execution +---------------------------------- + +For synchronous execution across multiple boards, use the `run_multi_decimated` helper function (see the :doc:`/tutorials/10_Multi_Board_Synchronization` notebook for details). + +Files Reference +--------------- + +All Pyro4 files are available in the QICK repository: + +- `00_nameserver.ipynb `_ - Tutorial for starting and verifying the name server +- `01_server.ipynb `_ - Tutorial for starting Pyro servers on QICK boards +- `02_client.ipynb `_ - Tutorial for connecting clients to multiple boards +- `nameserver.sh `_ - Shell script to start the name server +- `pyro_service.py `_ - Python module for Pyro service management +- `qick_pyro.service `_ - systemd service file for auto-starting Pyro on boot + +See Also +-------- + +- :doc:`xcom` - Hardware-level low-latency multi-board synchronization +- :doc:`/tutorials/10_Multi_Board_Synchronization` - Software synchronization with external clock and start signals +- `Pyro4 Documentation `_ +- `QICK pyro4 directory on GitHub `_ diff --git a/docs/source/topics/readout_modes.rst b/docs/source/topics/readout_modes.rst new file mode 100644 index 000000000..dd3970159 --- /dev/null +++ b/docs/source/topics/readout_modes.rst @@ -0,0 +1,103 @@ +Accumulated vs. decimated readout +================================== + +Every QICK program that reads data back uses one of two acquisition modes, exposed +by :class:`.AcquireMixin` (mixed into :class:`.AveragerProgramV2` and the older +tProc v1 averager classes) as two mutually exclusive methods: + +* :meth:`.AcquireMixin.acquire` -- **accumulated** readout. +* :meth:`.AcquireMixin.acquire_decimated` -- **decimated** readout. + +They read the same hardware trigger and window, but the readout IP integrates the +samples differently before they ever reach software, and the two methods return +data with different shapes for different purposes. You call one or the other, not +both, for a given run of the tProc program (though you can compile a program once +and call `acquire`/`acquire_decimated` on different runs of it, as long as you +haven't changed the number of readout windows). + +Accumulated (``acquire``) +-------------------------- + +The averager readout (``axis_avg_buffer``/``mr_buffer_et``, depending on channel) +integrates every sample in the readout window in hardware, producing a single +complex (I, Q) point per trigger. ``acquire()`` collects one such point per shot, +per declared readout channel, and returns it averaged over reps (and, if +``rounds>1``, over software rounds too): + +* Use this for the vast majority of experiments: anything where you care about + *one number per shot* -- a qubit state population, a resonator response at one + frequency, an S-parameter point. +* Supports software thresholding (``threshold``/``angle``) to convert the + averaged I/Q point into single-shot 0/1 classification before averaging -- + see :meth:`.AcquireMixin.get_shots`. +* Returns much less data than decimated mode, so it comfortably supports large + sweeps (many reps x many expts). +* Raw (pre-averaging, pre-length-normalization) accumulated data is available via + :meth:`.AcquireMixin.get_raw`; per-round data via :meth:`.AcquireMixin.get_rounds`. + +Decimated (``acquire_decimated``) +----------------------------------- + +The decimating readout streams out the *time-domain* I/Q waveform of the readout +window itself (after the readout's own decimation stage, not the raw ADC rate -- +see :doc:`/firmware/index` for the DDC/decimation chain), instead of a single integrated +point. + +* Use this to look at the readout waveform directly: verifying that your readout + window lines up with the pulse you expect to measure, debugging a bad envelope + or gain setting, or eyeballing a resonator ringdown. +* Returns one waveform per shot per readout trigger -- much larger than + accumulated mode. Buffer memory limits how much you can capture at once (see + the ``buf_maxlen`` check in the method's ``RuntimeError`` if you request too + much); it is not meant for large parameter sweeps. +* No thresholding option -- thresholding operates on the single accumulated + point, which decimated mode doesn't produce. +* Pair with :meth:`.AcquireMixin.get_time_axis` to get a time axis (in us) for + plotting. + +Common setup +------------ + +Both methods require the acquisition to be described first, via +:meth:`.AcquireMixin.setup_acquire` (which needs the tProc counter address, the +loop dimensions, and which loop level to average over -- see +:meth:`.AcquireMixin.setup_counter` for the subset of that needed if you have no +readouts at all, e.g. a pure-generator program you still want a progress bar +for). In practice you rarely call this yourself: :class:`.AveragerProgramV2` +(and the tProc v1 averager classes) call it for you in ``__init__``, from +``reps`` and any loops added with ``add_loop()``. You only need +:meth:`.AcquireMixin.setup_acquire` directly if you build a program on the +lower-level :class:`.AcquireProgramV2`/:class:`.AcquireMixin` without going +through :class:`.AveragerProgramV2`. Both ``acquire()``/``acquire_decimated()`` +accept the same ``rounds``, ``start_src``, ``remove_offset``, and +``step_rounds`` arguments; ``step_rounds=True`` in either case hands control of +the round-by-round loop to your own code via +``prepare_round()``/``finish_round()``/``finish_acquire()``, if you need to +interleave other work between rounds. + +.. list-table:: + :header-rows: 1 + :widths: 25 35 40 + + * - + - ``acquire()`` + - ``acquire_decimated()`` + * - Data per shot + - one (I, Q) point per readout + - one (I, Q) waveform per readout + * - Typical use + - experiments, sweeps + - debugging readout windows/pulses + * - Thresholding + - yes (``threshold``/``angle``) + - no + * - Data volume + - small + - large -- watch buffer limits + +See also +-------- + +* :doc:`/firmware/readouts/index` -- the readout hardware itself (DDC, decimation, buffers). +* :doc:`timing` -- how readout windows relate to the tProc's timed queues. +* :doc:`freq_matching` -- keeping generator and readout frequencies in sync. diff --git a/docs/topics/reference_clock.rst b/docs/source/topics/reference_clock.rst similarity index 100% rename from docs/topics/reference_clock.rst rename to docs/source/topics/reference_clock.rst diff --git a/docs/source/topics/timing.rst b/docs/source/topics/timing.rst new file mode 100644 index 000000000..04e8b0ad4 --- /dev/null +++ b/docs/source/topics/timing.rst @@ -0,0 +1,202 @@ +Timing with tProcessor v2 +========================= + +The tProcessor v2 uses a precise timing model based on three time variables. +Understanding these is essential for accurate waveform sequencing. + +.. contents:: + :local: + :depth: 2 + +Overview +-------- + +The tProcessor v2 has a **48-bit absolute time counter** (`t_abs`) that runs at the DAC clock frequency (`t_clk`). This counter is shared by all output channels (wave ports, data ports, triggers). + +Key concepts: +- **Deterministic latency**: ~5 `t_clk` cycles from instruction to output +- **Signed output times**: You can schedule outputs "before" the reference time +- **Non-blocking writes**: Instructions write to FIFOs and continue immediately + +Time Variables +-------------- + +.. list-table:: Time Variables + :header-rows: 1 + :widths: 20 20 60 + + * - Variable + - Width + - Description + * - `t_abs` + - 48 bits + - Absolute time counter. Runs at DAC clock (`t_clk`). Counts from last reset. + * - `t_ref` + - 48 bits + - Reference time offset. Defines where user time = 0. + * - `t_usr` + - 32 bits (read via `s_usr_time`) + - User time = `t_abs - t_ref` (truncated to 32 bits, signed) + * - `t_out_user` + - 32 bits (signed) + - Time specified in port write instructions (`@value` or `@s_out_time`) + * - `t_abs_out` + - 48 bits + - Absolute output time = `t_ref + t_out_user` (calculated by hardware) + +**Why is `t_out_user` signed?** +You can schedule outputs with a **negative** offset. For example, if you set `t_ref` to a future time, negative `t_out_user` allows outputs to occur "before" the new reference. This is useful for synchronizing multiple boards or for pre-calculated sequences. + +Time Instructions +----------------- + +The `TIME` instruction controls the time counters: + +.. code-block:: text + + TIME rst // t_abs = 0, t_ref = 0, core reset + TIME set_ref rX // t_ref = rX (t_abs unchanged) + TIME inc_ref #N // t_ref = t_ref + N + TIME updt #N // t_abs = t_abs + N (rarely used) + +**Typical initialization sequence:** + +.. code-block:: text + + TIME rst // Start from zero + // ... wait for external trigger ... + TIME set_ref r0 // Set t_ref to current t_abs (t_usr becomes 0) + +Scheduling Outputs +------------------ + +**Fixed absolute time (using immediate):** + +.. code-block:: text + + WPORT_WR p1 r_wave @1000 // Output when t_usr reaches 1000 + +**Relative to previous output (using s_out_time):** + +.. code-block:: text + + REG_WR s_out_time imm #1000 + WPORT_WR p1 r_wave @s_out_time + REG_WR s_out_time op -op(s_out_time + #500) + WPORT_WR p1 r_wave @s_out_time // 500 cycles after first + +**Using a register as time source:** + +.. code-block:: text + + REG_WR r0 imm #2000 + WPORT_WR p1 r_wave @r0 // Output when t_usr reaches value in r0 + +Waiting for Time +---------------- + +The `WAIT time @N` macro pauses the tProc until `t_usr >= N`: + +.. code-block:: text + + WAIT time @5000 // Pause execution until t_usr >= 5000 + +**Expansion:** + +.. code-block:: text + + // WAIT time @5000 expands to: + TEST -op(s_usr_time - #5000) + JUMP HERE -if(S) // Loop while s_usr_time < 5000 + +Dispatcher Latency +------------------ + +When you execute a port write instruction (`WPORT_WR`, `DPORT_WR`, `TRIG`), the data is not output immediately. Instead: + +1. **Cycle 1**: CPU writes to FIFO (in `c_clk` domain) +2. **Cycles 2-3**: Clock domain crossing to `t_clk` domain +3. **Cycle 4**: Dispatcher compares `t_abs` with scheduled time +4. **Cycle 5**: Output is updated on the physical pin + +**Total latency:** Approximately **5 `t_clk` cycles** (about 13 ns at 384 MHz). + +.. important:: + This latency is **deterministic** and constant. You can compensate for it by + subtracting 5 cycles from your scheduled times if you need output aligned with + a specific `t_abs` value. + +FIFO Behavior +------------- + +Each output type has its own FIFO: + +- **WFIFO** (Wave): 168-bit data + 48-bit time, depth 8-512 +- **DFIFO** (Data): 32-bit data + 48-bit time, depth 8-512 +- **TFIFO** (Trigger): 1-bit data + 48-bit time, depth 8-512 + +When a FIFO becomes full: +- Default behavior: Core **stalls** until space is available +- With `DISABLE_FIFO_FULL_PAUSE` (tproc_cfg[10]=1): Writes are dropped (data loss) + +**Check FIFO status:** + +.. code-block:: text + + TEST -op(s_status AND #0x4000) // Test FIFO_FULL bit (bit 14) + JUMP FIFO_OK -if(Z) // Continue if not full + +Clock Domains +------------- + +The tProcessor has three clock domains: + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Domain + - Clock + - Purpose + * - Core (`c_clk`) + - ≤ 350 MHz + - CPU pipeline, registers, ALU, memories + * - Time (`t_clk`) + - DAC frequency (384 MHz - 6 GHz) + - Dispatcher, FIFOs, time counter, output ports + * - PS (`ps_clk`) + - 100 MHz + - AXI-Lite and AXI-Stream communication + +**Rule:** `t_clk` must be ≥ `c_clk`. Never run the core faster than the time counter. + +Related Instructions +-------------------- + +- `TIME rst` - Reset time counters +- `TIME set_ref` - Set reference time +- `TIME inc_ref` - Increment reference time +- `WAIT time @N` - Wait for specific user time +- `CLEAR` - Clear status flags (not directly time-related) + +Common Pitfalls +--------------- + +**Pitfall 1: Forgetting that `s_out_time` is signed** + +Negative values are allowed. If you accidentally write a negative time, the output will happen immediately (or when `t_abs` wraps around). + +**Pitfall 2: Back-to-back TIME instructions** + +TIME instructions cross clock domains. Do not execute two TIME instructions with less than 5 `c_clk` cycles between them. + +**Pitfall 3: FIFO overflow in tight loops** + +If you write ports faster than the dispatcher can process, the FIFO will fill and the core will stall. Monitor `s_status[14]` (FIFO_FULL). + +Related Documentation +--------------------- + +* :doc:`/tprocv2_trm` - Complete tProcessor v2 reference manual +* :doc:`/firmware/index` - Firmware overview and clock frequencies +* :doc:`../tprocv2_trm` - Time management section in main manual diff --git a/docs/source/topics/tprocv2_migration.rst b/docs/source/topics/tprocv2_migration.rst new file mode 100644 index 000000000..840c3125d --- /dev/null +++ b/docs/source/topics/tprocv2_migration.rst @@ -0,0 +1,138 @@ +Migrating from tProc v1 +======================== + +.. note:: + Content originally contributed by `Jay Chien `_ + (`PR #412 `_), adapted here + for the current docs structure and verified against ``qick.asm_v2``. + +tProc v2 keeps the same basic role as tProc v1 — sequencing pulses, triggers, +readout windows, and feedback — but reorganizes both the firmware interface and +the Python programming model. This page is a short migration guide for users +who already know tProc v1 and want to understand the most important changes +before writing tProc v2 programs. For the full picture, see :doc:`playing_pulses`, +:doc:`timing`, and :doc:`units`. + +Hardware sweeps +---------------- + +In tProc v1 programs, hardware sweeps often required an ``update()`` method +with explicit register math. For example, a gain sweep might update the +generator's gain register with ``mathi()`` after each shot: + +.. code-block:: python + + class Program(RAveragerProgram): + def initialize(self): + self.q_rp = self.ch_page(self.cfg["qubit_ch"]) + self.r_gain = self.sreg(self.cfg["qubit_ch"], "gain") + + def body(self): + ... + + def update(self): + self.mathi(self.q_rp, self.r_gain, self.r_gain, "+", self.cfg["step"]) + + gain_list, avgi, avgq = prog.acquire(soc) + +tProc v2 provides higher-level sweep helpers. A program declares a loop with +:meth:`~.QickProgramV2.add_loop`, attaches a swept parameter to that loop with +:func:`~.QickSweep1D`, and later queries the compiled program for the actual +sweep points: + +.. code-block:: python + + class Program(AveragerProgramV2): + def _initialize(self, cfg): + self.add_loop("gainloop", cfg["steps"]) + self.add_pulse( + ch=cfg["qubit_ch"], + name="qubit_pulse", + style="const", + freq=cfg["freq"], + phase=0, + gain=QickSweep1D("gainloop", cfg["gain_start"], cfg["gain_stop"]), + length=cfg["length"], + ) + + def _body(self, cfg): + self.pulse(ch=cfg["qubit_ch"], name="qubit_pulse", t=0) + + iq_list = prog.acquire(soc, rounds=avg, progress=False) + gains = prog.get_pulse_param("qubit_pulse", "gain", as_array=True) + +Common swept parameters include frequency, length, time, and gain. For +frequency, length, and gain sweeps, use ``get_pulse_param()`` to query the +compiled sweep points: + +.. code-block:: python + + freqs = prog.get_pulse_param("qubit_pulse", "freq", as_array=True) + lengths = prog.get_pulse_param("qubit_pulse", "length", as_array=True) + gains = prog.get_pulse_param("qubit_pulse", "gain", as_array=True) + +For timing sweeps, tag the timed instruction (``delay``, ``delay_auto``, ...) +and use ``get_time_param()``: + +.. code-block:: python + + times = prog.get_time_param("time_tag", "t", as_array=True) + +Timing +------ + +In tProc v2, pulse and trigger times are written in user-facing units, usually +microseconds. The Python API converts these values to tProc timing-clock cycles +when the program is compiled. See :doc:`timing` for the underlying clock model. + +Some tProc v1 timing calls have renamed equivalents in ``QickProgramV2``: + +.. list-table:: + :header-rows: 1 + :widths: 30 30 40 + + * - tProc v1 + - tProc v2 + - Notes + * - ``sync_all()`` + - ``delay_auto()`` + - Advances the reference time to the end of the last pulse/readout, plus an optional margin. + * - ``synci(t)`` + - ``delay(t)`` + - Advances the reference time by a fixed amount, independent of pulse/readout end times. + * - (no direct v1 equivalent) + - ``wait_auto()`` / ``wait(t)`` + - Pauses execution without moving the reference time; see :doc:`timing`. + +Playing pulses +-------------- + +In tProc v1, pulse parameters were typically loaded into generator registers +right before playing a pulse. In tProc v2, pulse settings are stored as +waveforms in waveform memory ahead of time: a pulse can bundle one or more +waveforms, and ``pulse()`` just tells the generator which waveform-memory entry +to play. This makes it easy to define several pulses during initialization and +choose which one to play in the program body. See :doc:`playing_pulses` for the +full workflow. + +A typical tProc v2 program: + +* Declares the generator and readout channels (``declare_gen()``, ``declare_readout()``). +* Adds pulse definitions in ``_initialize()`` (``add_envelope()``, ``add_pulse()``). +* Plays those pulses in ``_body()`` with ``pulse()``. +* Uses ``trigger()`` and ``acquire()``/``acquire_decimated()`` when readout data is needed. + +Units +----- + +tProc v2 APIs generally use natural units instead of raw register values: + +* Times and pulse lengths are in microseconds. +* Frequencies are in MHz. +* Phases are in degrees. +* Gains are normalized values, usually from ``-1`` to ``1``. + +The compiler rounds these values to the nearest supported hardware +representation. To inspect the rounded values actually used by a compiled +program, use ``get_pulse_param()`` and ``get_time_param()``. See :doc:`units` +for the full reference. diff --git a/docs/source/topics/units.rst b/docs/source/topics/units.rst new file mode 100644 index 000000000..c940f7491 --- /dev/null +++ b/docs/source/topics/units.rst @@ -0,0 +1,66 @@ +Time units +========== + +Time durations are generally specified in units of clock cycles. +The relevant clocks are the tProcessor clock (`c_clk`) and the fabric clocks of the generators and readouts. +In general these can all be different (and can even vary among generators or readouts). + +tProcessor Time +--------------- + +The tProcessor uses a 48-bit absolute time counter (`t_abs`) that runs at the DAC clock frequency (`t_clk`). +Time values in tProc assembly (e.g., `@1000`, `@s_out_time`) are in units of `t_clk` cycles. + +**Typical clock frequencies:** + +- `t_clk` (DAC clock): 384 MHz to 6 GHz +- `c_clk` (core clock): ≤ `t_clk` (typically 350-384 MHz) + +**Conversion helpers in Python:** + +For convenience, the :meth:`.QickConfig.us2cycles()` and :meth:`.QickConfig.cycles2us()` methods convert between floating-point times and integer cycles. +You must specify which clock you are using: + +.. list-table:: Clock Selection for us2cycles() + :header-rows: 1 + :widths: 30 40 30 + + * - Parameter Type + - Use + - Example + * - Generator (pulse length, sigma) + - ``gen_ch`` (generator channel index) + - ``soccfg.us2cycles(1e-6, gen_ch=0)`` + * - Readout (acquisition length) + - ``ro_ch`` (readout channel index) + - ``soccfg.us2cycles(1e-6, ro_ch=0)`` + * - tProcessor (wait, sync, delays) + - No channel parameter + - ``soccfg.us2cycles(1e-6)`` + +**Example:** + +.. code-block:: python + + from qick import * + + soc = QickSoc() + soccfg = soc # QickSoc is itself a QickConfig, so soc has all these methods directly + + # Convert 1 microsecond to tProc cycles + tproc_cycles = soccfg.us2cycles(1e-6) + print(f"1 us = {tproc_cycles} tProc cycles") + + # Convert 1 us to generator 0 cycles + gen_cycles = soccfg.us2cycles(1e-6, gen_ch=0) + print(f"1 us = {gen_cycles} generator cycles") + +Note: tProc time values (`s_out_time`, `@time` in assembly) are **signed 32-bit integers**. +When using `us2cycles()`, ensure the result fits within ±2^31-1. + +Related Documentation +--------------------- + +* :doc:`/tprocv2_trm` - tProcessor timing model +* :doc:`/firmware/generators/sg_v6` - Signal Generator v6 documentation +* :doc:`/firmware/readouts/index` - Readout system documentation diff --git a/docs/source/topics/xcom.rst b/docs/source/topics/xcom.rst new file mode 100644 index 000000000..44300f3e2 --- /dev/null +++ b/docs/source/topics/xcom.rst @@ -0,0 +1,485 @@ +.. _xcom: + +XCOM: Multi-Board Network Synchronization and Communication +============================================================ + +XCOM (X - Communication) is a **full mesh network** that enables precise synchronization and low-latency communication between multiple QICK boards. This module is essential for scaling quantum control systems beyond a single RFSoC board. + +For a detailed description of the XCOM architecture and performance, refer to the XCOM paper: `arXiv:2603.18977v1 `_. + +Overview +-------- + +XCOM provides two primary capabilities: + +1. **Synchronization**: Aligns multiple QICK boards with <100 ps skew, free of drift and loss of lock +2. **Communication**: Provides deterministic, all-to-all simultaneous data exchange with latency below 185 ns (can be reduced to 62 ns) + +The XCOM network supports up to **15 boards** (prototype supports 5) and is designed to scale to large quantum computing systems. + +XCOM can be operated from: + +- **Python scripts** (via AXI-lite interface, higher latency due to Linux overhead) +- **tProc programs** (real-time operation at fabric clock speed, lowest latency) + +.. figure:: ../images/topics/fig1_xcom_block_diag.png + :align: center + :alt: XCOM Architecture + + XCOM block architecture (source: XCOM paper) + +Hardware Requirements +--------------------- + +XCOM requires additional hardware components: + +1. **FMC Transceiver Board**: A small board that plugs into the FMC connector of each ZCU216 board. It routes LVDS signals to RJ45 connectors. + +2. **External Hub**: A fanout hub that copies each transmitter's signal to all receivers, enabling full mesh connectivity. + +The current hardware prototype supports up to 5 boards, while the firmware IP already supports up to 15 boards and can be extended further. + +.. figure:: ../images/topics/fig3_fanout_box.png + :align: center + :alt: XCOM Hardware Setup + + XCOM hardware configuration (source: XCOM paper) + +Clock Domains +------------- + +The XCOM block operates across three clock domains: + +.. list-table:: + :header-rows: 1 + :widths: 15 25 60 + + * - Domain + - Source + - Description + * - ``t_clk`` + - tProc timing clock + - Fastest clock; used for high-speed serial communication + * - ``c_clk`` + - tProc core clock + - Used for command processing and state machines + * - ``ps_clk`` + - Processing System clock + - Used for AXI-lite interface to Python + +It is assumed that ``t_clk`` ≥ ``c_clk`` ≥ ``ps_clk`` in all configurations. + +Communication Protocol +---------------------- + +XCOM uses two LVDS signals for point-to-point communication, following a **Data Strobe Encoding** scheme: + +- **``xclk``** (strobe): Each change indicates a new value is present on the data line +- **``xdata``** (serial data): Carries the serialized command and data + +.. figure:: ../images/topics/xcom_waves.png + :align: center + :alt: XCOM Data Strobe Encoding + + Data strobe encoding example for 8-bit data transmission + +The ``xcom_cfg`` register's tick-config field (``i_cfg_tick``) sets the period of the strobe signal in ``t_clk`` cycles. The relationship is linear (period = 2 × (value + 1)), not a power-of-two doubling, and permitted values range from 0 to 7: + +.. list-table:: + :header-rows: 1 + :widths: 15 35 + + * - Value + - Strobe period (t_clk cycles) + * - 0 + - 2 + * - 1 + - 4 + * - 2 + - 6 + * - 3 + - 8 + * - 4 + - 10 + * - 5 + - 12 + * - 6 + - 14 + * - 7 + - 16 + +Interface Signals +----------------- + +The XCOM peripheral connects to the QICK processor via a dedicated interface with the following signals: + +.. list-table:: + :header-rows: 1 + :widths: 25 15 60 + + * - Signal + - Direction + - Description + * - ``qp_en_i`` + - Input + - Enable; activates peripheral on rising edge + * - ``qp_op_i[4:0]`` + - Input + - 5-bit operation code + * - ``qp_dt1_i[31:0]`` + - Input + - 32-bit data 1 from processor to peripheral (drives ``AXI_DT1``/``core_data1``) + * - ``qp_dt2_i[31:0]`` + - Input + - 32-bit data 2 from processor to peripheral (drives ``AXI_DT2``/``core_data2``) + * - ``qp_rdy_o`` + - Output + - Indicates peripheral ready status + * - ``qp_vld_o`` + - Output + - Marks valid data from peripheral + * - ``qp_dt1_o[31:0]`` + - Output + - Data 1 from peripheral to processor + * - ``qp_dt2_o[31:0]`` + - Output + - Data 2 from peripheral to processor + * - ``qp_flag_o`` + - Output + - Flag for inter-board signaling + +The generic QICK peripheral template (``qick_periph.sv``) actually exposes four +32-bit data inputs (``qp_dt1_i``..``qp_dt4_i``) and two 32-bit data outputs +(``qp_dt1_o``, ``qp_dt2_o``); XCOM only uses the first two inputs and both +outputs, matching its two-register (``DT1``/``DT2``) command payload. + +.. figure:: ../images/topics/xcom_periph_timing.png + :align: center + :alt: XCOM Peripheral Timing + + Peripheral interface timing diagram + +Python Interface +---------------- + +The ``QICK_Xcom`` class provides Python access to the XCOM hardware through AXI-lite registers. + +AXI Registers +~~~~~~~~~~~~~ + +.. figure:: ../images/topics/xcom_registers.png + :align: center + :alt: XCOM Registers + + XCOM AXI register map + +The following registers are available: + +.. list-table:: + :header-rows: 1 + :widths: 20 15 10 55 + + * - Register + - Address + - Access + - Description + * - ``xcom_ctrl`` + - 0 + - R/W + - Command control register + * - ``xcom_cfg`` + - 1 + - R/W + - Configuration register + * - ``axi_dt1`` + - 2 + - R/W + - Data register 1 + * - ``axi_dt2`` + - 3 + - R/W + - Data register 2 + * - ``axi_addr`` + - 4 + - R/W + - Address register + * - ``board_id`` + - 6 + - R + - Local board ID + * - ``flag`` + - 7 + - R + - Local flag status + * - ``dt1`` + - 8 + - R + - Received data 1 + * - ``dt2`` + - 9 + - R + - Received data 2 + * - ``mem`` + - 10 + - R + - Memory readback + * - ``rx_dt`` + - 12 + - R + - RX data (debug) + * - ``tx_dt`` + - 13 + - R + - TX data (debug) + * - ``status`` + - 14 + - R + - Status register + * - ``debug`` + - 15 + - R + - Debug register + +Register Control +~~~~~~~~~~~~~~~~ + +The ``XCOM_CTRL`` register is used to send commands. The ``EN`` field (``ctrl[0]``) auto-resets after one clock cycle, ensuring commands are executed only once. + +.. figure:: ../images/topics/xcom_reg_timing.png + :align: center + :alt: XCOM Control Timing + + ``XCOM_CTRL`` register timing diagram + +When ``EN`` is high: + +- ``OP_I`` (``ctrl[5:1]``) contains the command to execute +- ``AXI_DT1`` and ``AXI_DT2`` contain data for the command + +Commands +-------- + +XCOM supports two types of commands: + +- **NET commands**: Sent over the network to other boards (OP code MSB = 0) +- **LOC commands**: Executed locally, affecting only this board (OP code MSB = 1) + +NET Commands +~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 10 15 45 + + * - Command + - Code + - OP + - Function + * - ``XCOM_CLEAR_FLAG`` + - 0 + - 00000 + - Clear flag on remote board + * - ``XCOM_SET_FLAG`` + - 1 + - 00001 + - Set flag on remote board + * - ``XCOM_SEND_8BIT_1`` + - 2 + - 00010 + - Send 8-bit data to qp_dt1 + * - ``XCOM_SEND_8BIT_2`` + - 3 + - 00011 + - Send 8-bit data to memory + * - ``XCOM_SEND_16BIT_1`` + - 4 + - 00100 + - Send 16-bit data to qp_dt1 + * - ``XCOM_SEND_16BIT_2`` + - 5 + - 00101 + - Send 16-bit data to memory + * - ``XCOM_SEND_32BIT_1`` + - 6 + - 00110 + - Send 32-bit data to qp_dt1 + * - ``XCOM_SEND_32BIT_2`` + - 7 + - 00111 + - Send 32-bit data to memory + * - ``XCOM_QRST_SYNC`` + - 8 + - 01000 + - Synchronize with external PPS + * - ``XCOM_AUTO_ID`` + - 9 + - 01001 + - Auto-assign board ID + * - ``XCOM_UPDATE_DT8/16/32`` + - 10, 12, 14 + - 01010, 01100, 01110 + - Future use + * - ``XCOM_QCTRL`` + - 11 + - 01011 + - Control tProc state + +LOC Commands +~~~~~~~~~~~~ + +.. list-table:: + :header-rows: 1 + :widths: 30 10 15 45 + + * - Command + - Code + - OP + - Function + * - ``XCOM_SET_ID`` + - 16 + - 10000 + - Set local board ID + * - ``XCOM_WRITE_FLAG`` + - 17 + - 10001 + - Write local flag + * - ``XCOM_WRITE_REG`` + - 18 + - 10010 + - Write to qp_dt register + * - ``XCOM_WRITE_MEM`` + - 19 + - 10011 + - Write to internal memory + * - ``XCOM_RST`` + - 31 + - 11111 + - Reset XCOM block + +For a complete reference, see :doc:`XCOM-commands`. + +tProc Control via XCOM +---------------------- + +The ``XCOM_QCTRL`` command enables remote control of tProc state. The ``DT2[2:0]`` field selects the operation: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - DT2[2:0] + - Operation + * - 2 + - Time reset + * - 3 + - Time update + * - 4 + - Core start + * - 5 + - Core stop + * - 6 + - Processor start + * - 7 + - Processor stop + +Synchronization Protocol +------------------------ + +XCOM synchronization follows a multi-step process: + +1. **Frequency synchronization**: All boards share a common external reference clock (e.g., 100 MHz rubidium) +2. **Phase synchronization**: PLLs configured in nested zero-delay mode +3. **DAC/ADC alignment**: Multi-tile sync (MTS) calibration +4. **Absolute time alignment**: Master broadcasts RESET and START commands via ``XCOM_QRST_SYNC`` + +The synchronization sequence: + +1. Designate one board as master +2. Run ``AUTO_ID`` to assign network IDs +3. Master broadcasts ``XCOM_QRST_SYNC`` to reset absolute clocks +4. Master broadcasts ``XCOM_QCTRL`` to start all tProcs simultaneously + +This achieves <100 ps skew between boards, enabling truly synchronized multi-board experiments. + +Performance +----------- + +.. list-table:: + :header-rows: 1 + :widths: 50 50 + + * - Metric + - Value + * - Inter-board skew + - <100 ps + * - Latency (32-bit word) + - 186 ns @ 100 MHz + * - Latency (32-bit word, projected) + - 62 ns @ 312.9 MHz + * - Maximum boards + - 15 (firmware), 5 (current prototype) + * - Clock frequency + - Up to 312.9 MHz (LVDS HP I/O) + +Debugging +--------- + +The XCOM block provides extensive debug capabilities through dedicated registers. + +Status Register +~~~~~~~~~~~~~~~ + +The ``status`` register provides real-time status information: + +.. list-table:: + :header-rows: 1 + :widths: 20 15 65 + + * - Field + - Bits + - Description + * - ``tx_st`` + - [1:0] + - Transmitter state (0=IDLE, 1=WVLD, 2=WSYNC, 3=WRDY) + * - ``rx_st`` + - [8:6] + - Receiver state (0=IDLE, 1=HEADER, 2=DATA, 3=REQ, 4=ACK) + * - ``tx_ready`` + - 16 + - Transmitter ready + * - ``board_id`` + - [20:17] + - Local board ID + * - ``rx_data_cntr`` + - [24:21] + - Received data counter + +Debug Register +~~~~~~~~~~~~~~ + +The ``debug`` register exposes internal state machine signals for advanced debugging. + +Python Debug Methods +~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: python + + # Print all AXI registers + soc.xcom_0.print_axi_regs() + + # Print status information + soc.xcom_0.print_status() + + # Print debug information + soc.xcom_0.print_debug() + +See Also +-------- + +* :doc:`XCOM-commands` — Complete command reference + +* :doc:`../tutorials/14_XCOM_Network_Synchronization` — XCOM demonstration notebook + +* `XCOM paper (arXiv:2603.18977v1) `_ + +* `QICK GitHub repository `_ diff --git a/docs/source/tprocv1.rst b/docs/source/tprocv1.rst new file mode 100644 index 000000000..0bacf0e40 --- /dev/null +++ b/docs/source/tprocv1.rst @@ -0,0 +1,642 @@ +======================================================== +tProcessor v1 (axis_tproc64x32_x8) - reference manual +======================================================== + +.. contents:: + :local: + :depth: 2 + +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 + :doc:`/tprocv2_trm`. + + 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=16`` parameter the usable program memory is + 2\ :sup:`13` = 8192 instructions (64 KB) -- see + :ref:`tprocv1-pmem-depth` for how that number is derived from the RTL. + v2 instructions are 72 bits, with program memory depth set independently + by the ``PMEM_AW`` parameter (default 2\ :sup:`8` = 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 :ref:`tprocv1-regfile`. v2 splits registers into + three purpose-built banks (general-purpose ``DREG``, special-function + ``SREG``, and waveform-parameter ``WREG``) described in + :ref:`tproc-registers` of the v2 manual. + * **Instruction set richness.** v1 has a fixed set of ~20 instructions + (see :ref:`tprocv1-isa`) 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 both + ``pushi``/``popi`` and 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 :ref:`tproc-quick-ref`). + +-------------------------------------------------------------------- +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 :ref:`tprocv1-isa`). The instruction-format comments at + the top of ``ctrl.sv`` are the authoritative bit-level encoding reference + and were cross-checked against the ``instructions`` dictionary in + ``qick_lib/qick/asm_v1.py`` for 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 :ref:`tprocv1-regfile`. +* **ALU** (``alu/alu.v`` + ``math.vhd`` + ``bitw.vhd``) -- bitwise and + arithmetic operations on 32-bit operands, plus a second, independent adder + (in ``tproc64x32_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 by ``pushi``/``popi`` (and internally by ``loopnz`` + for its decrement-and-branch loop counter). +* **Per-channel timed-output dispatcher** (``fifo/fifo.vhd`` + + ``timed_ictrl.vhd``, instantiated 8x, one per output channel) -- see + :ref:`tprocv1-output`. +* **Data memory** (``bram_dp`` instance in ``axis_tproc64x32_x8.v`` + + arbiter in ``data_mem/``) -- a dual-port, 32-bit BRAM shared between the + core's own ``memr``/``memw`` instructions and PS access; see + :ref:`tprocv1-dmem`. + +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 the + ``read`` instruction. + +-------------------------------------------------------------------- +2. Synthesis Parameters +-------------------------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 15 65 + + * - Parameter + - RTL Default + - Description + * - ``PMEM_N`` + - 16 + - Program-memory address width, in the sense of the ``pmem_addr`` output + port width. As explained in :ref:`tprocv1-pmem-depth`, the *usable* + instruction depth is 2\ :sup:`PMEM_N-3`, not 2\ :sup:`PMEM_N` -- with + the default value of 16 that is 8192 64-bit instructions (64 KB). + Exposed to Python as ``soc.tproc.PMEM_N``; the actual program-memory + *size* used by the driver, however, is read back from the + instantiated memory object's byte length + (``self.mem.mmio.length // 8``), not computed from ``PMEM_N``. + * - ``DMEM_N`` + - 10 + - Data-memory address width (log2 of the number of 32-bit words). With + the default value, data memory is 2\ :sup:`10` = 1024 samples (4 KB). + Exposed to Python as ``soc.tproc.DMEM_N`` and ``soc.tproc.cfg['dmem_size'] + = 2**DMEM_N``. + +.. 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 + :ref:`tprocv1-pmem-depth` and :ref:`tprocv1-regfile`). + +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 :ref:`tprocv1-regmap`. + +-------------------------------------------------------------------- +3. Datapath / Architecture +-------------------------------------------------------------------- + +.. _tprocv1-pmem-depth: + +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 2**:sup:`13` = 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). + +.. _tprocv1-regfile: + +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. + +.. _tprocv1-output: + +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``). + +.. _tprocv1-dmem: + +3.8. Data Memory +-------------------- + +Data memory is a dual-port 32-bit BRAM (``bram_dp``, depth 2\ :sup:`DMEM_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 :ref:`tprocv1-regmap`) map directly onto data-memory samples. +* **Bulk AXI-Stream DMA**, gated by ``MEM_MODE_REG`` (0=read from memory to + ``m0_axis``, 1=write from ``s0_axis`` to memory), ``MEM_START_REG``, + ``MEM_ADDR_REG`` (start address) and ``MEM_LEN_REG`` (sample count); the + DMA transaction runs until ``LEN_REG`` samples have moved (read side) or + ``s_axis_tlast`` is 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. + +-------------------------------------------------------------------- + +.. _tprocv1-isa: + +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``): + +.. list-table:: Instruction Formats + :header-rows: 1 + :widths: 15 15 70 + + * - Format + - Bits ``63:56`` + - Remaining fields + * - I-Type + - opcode + - ``page`` (55:53), ``channel`` (52:50), ``oper`` (49:46), ``ra`` + (45:41), ``rb`` (40:36), ``rc`` (35:31), 31-bit sign-extended + ``imm`` (30:0) + * - J-Type + - opcode + - ``page`` (55:53), ``oper`` (49:46), up to 3 register fields + (45:31), 16-bit jump ``addr`` (15:0) + * - R-Type + - opcode + - ``page`` (55:53), ``channel`` (52:50), ``oper`` (49:46), up to 7 + register fields (45:6) + +.. list-table:: Instruction Set + :header-rows: 1 + :widths: 12 10 12 66 + + * - Mnemonic + - Format + - Opcode + - Description + * - ``pushi`` + - I + - ``0b00010000`` + - Push register ``ra`` (page ``p``) onto the stack; load immediate + ``imm`` into register ``rb``. + * - ``popi`` + - I + - ``0b00010001`` + - Pop the stack into register ``r`` (page ``p``). + * - ``mathi`` + - I + - ``0b00010010`` + - ``ra = rb imm``; ``op`` in ``{+, -, *}`` (``oper`` field). + * - ``seti`` + - I + - ``0b00010011`` + - Schedule register ``r`` (page ``p``) to be written to output + ``channel`` at time ``imm`` (non-blocking; see + :ref:`tprocv1-output`). + * - ``synci`` + - I + - ``0b00010100`` + - Rebase the internal time-sync reference to immediate ``imm``. + * - ``waiti`` + - I + - ``0b00010101`` + - Block the core on ``channel`` until master clock reaches ``imm``. + * - ``bitwi`` + - I + - ``0b00010110`` + - ``ra = rb imm``; ``op`` in ``{&, \|, ^, ~, <<, >>}``. + * - ``memri`` + - I + - ``0b00010111`` + - ``r = mem[imm]`` (page ``p``). + * - ``memwi`` + - I + - ``0b00011000`` + - ``mem[imm] = r`` (page ``p``). + * - ``regwi`` + - I + - ``0b00011001`` + - ``r = imm`` (page ``p``). + * - ``setbi`` + - I + - ``0b00011010`` + - Blocking variant of ``seti``. + * - ``loopnz`` + - J + - ``0b00110000`` + - If register ``r`` != 0: decrement ``r`` and jump to ``addr``; else + fall through. + * - ``condj`` + - J + - ``0b00110001`` + - Jump to ``addr`` if ``ra rb`` is true; ``op`` in + ``{>, >=, <, <=, ==, !=}`` (0-5, see the condition-code list at the + end of this section). + * - ``end`` + - J + - ``0b00111111`` + - Halt: jump to the terminal state until the tProc is restarted. + * - ``math`` + - R + - ``0b01010000`` + - ``ra = rb rc``; ``op`` in ``{+, -, *}``. + * - ``set`` + - R + - ``0b01010001`` + - Schedule 5 registers ``{re,rd,rc,rb,ra}`` (freq/phase/addr/gain/mode, + by convention) to be written to output ``channel`` at time ``rt`` + (non-blocking). + * - ``sync`` + - R + - ``0b01010010`` + - Rebase the internal time-sync reference to register ``r``. + * - ``read`` + - R + - ``0b01010011`` + - Read input ``channel`` (upper or lower 32 bits, by ``oper``) into + register ``r``. + * - ``wait`` + - R + - ``0b01010100`` + - Block the core on ``channel`` until master clock reaches register + ``r``. + * - ``bitw`` + - R + - ``0b01010101`` + - ``ra = rb rc``; ``op`` in ``{&, \|, ^, ~, <<, >>}``. + * - ``memr`` + - R + - ``0b01010110`` + - ``ra = mem[rb]``. + * - ``memw`` + - R + - ``0b01010111`` + - ``mem[rb] = ra``. + * - ``setb`` + - R + - ``0b01011000`` + - Blocking variant of ``set``. + +.. _tprocv1-condcodes: + +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). + +-------------------------------------------------------------------- + +.. _tprocv1-regmap: + +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: + +.. list-table:: + :header-rows: 1 + :widths: 22 12 12 54 + + * - Register + - Word Offset + - Byte Address + - Description + * - ``START_SRC_REG`` + - 0 + - 0x00 + - Bit 0: 0 = start from ``START_REG`` (internal), 1 = start from the + external ``start`` input pin. + * - ``START_REG`` + - 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 + ``start()``, which pulses it 0 then 1). + * - ``MEM_MODE_REG`` + - 2 + - 0x08 + - Bit 0: 0 = AXIS read (memory -> ``m0_axis``), 1 = AXIS write + (``s0_axis`` -> memory). + * - ``MEM_START_REG`` + - 3 + - 0x0C + - Bit 0: 0 = stopped, 1 = execute the configured AXIS DMA operation. + * - ``MEM_ADDR_REG`` + - 4 + - 0x10 + - Starting data-memory sample address for the AXIS DMA operation. + * - ``MEM_LEN_REG`` + - 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 :doc:`/tprocv2_trm`). It is reached as ``soc.tproc``. + +.. code-block:: python + + 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()`` -- pulses ``start_reg`` 0 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 + the ``end`` opcode (``0b00111111``, matching ``ctrl.sv``'s ``END_ST`` + encoding), 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 bits ``63:56`` of 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 + a ``reset()``) the cached program to program memory via a direct + ``mmio`` array 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 by ``NREG`` (see :ref:`tprocv1-regmap`). +* ``load_dmem(buff_in, addr)`` / ``read_dmem(addr, length)`` -- bulk data-memory + transfer via the AXI-Lite/DMA arbiter (:ref:`tprocv1-dmem`): configures + ``mem_mode_reg``/``mem_addr_reg``/``mem_len_reg``/``mem_start_reg`` and + drives an AXI DMA channel. +* ``start_src(src)`` -- ``"internal"``/``"external"``, sets ``start_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 :ref:`tprocv1-isa`, 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 (:ref:`tprocv1-isa`) by splitting values >= 2\ :sup:`30` +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 :ref:`tproc-registers`) 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 :ref:`tprocv1-isa`, 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 :ref:`tprocv1-output`, 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. + +-------------------------------------------------------------------- + +Related Documentation +---------------------- + +* :doc:`/tprocv2_trm` -- the current tProcessor (v2), recommended for new + designs. +* :doc:`/firmware/index` -- system-level firmware overview (signal generators, + readout, channel assignments). +* :doc:`/firmware/readouts/avg_buffer` -- Averager + Buffer readout block, whose ``trigger`` + input is typically driven from a v1 tProc output channel bit. +* `tProcessor_64_and_Signal_Generator_V4.pdf + `_ + -- an older learning-oriented write-up of the v1 tProcessor and the + signal generator it was originally paired with. The upstream ``qick`` + repository's own firmware overview explicitly flags this PDF as **not** + up to date with the current RTL; treat it as background reading only; the + bit-level facts on this page were verified directly against the RTL and + Python driver in this repository, not against that PDF. diff --git a/docs/source/tprocv2_trm.rst b/docs/source/tprocv2_trm.rst new file mode 100644 index 000000000..b61a1e649 --- /dev/null +++ b/docs/source/tprocv2_trm.rst @@ -0,0 +1,5813 @@ +.. _tprocv2_trm: + +================================================== +tProcessor v2 - reference manual +================================================== + +.. meta:: + :description: Complete reference manual for the QICK tProcessor v2 real-time co-processor + +:Version: 2.2 +:Last Update: 2026-09-04 +:Compatibility: QICK Firmware (>= v0.0.1) +:Audience: Firmware developers, advanced QICK users, researchers + +.. note:: + This is the complete reference manual for the qick_processor (tProcV2). + For system-level firmware overview (signal generators, readout, channel assignments), + see :doc:`/firmware/index`. If you are working with older, legacy tProc v1 + hardware (``axis_tproc64x32_x8``) instead, see :doc:`/tprocv1`. + + This document was updated to match the current RTL (``firmware/rtl_lib/``). + Key corrections: parameter defaults (PMEM_AW/DMEM_AW/WMEM_AW=8, REG_AW=4, + IN_PORT_QTY=1, OUT_TRIG_QTY=2), stack depth=8 (not 256), header encodings + (010/011 internal/external peripherals, not 111), ARITH formulas + (``(A op D)*B[+/-C]``, latency 4 cycles), condition codes (bits 9:7), + s_cfg/s_ctrl bit layouts, and s_status bit map. ``w2`` is ``w_env`` + (24-bit) and ``w3`` is ``w_gain`` (32-bit) in ``r_wave`` — confirmed + against the RTL (``out_wreg_o`` truncation in ``qcore_reg_bank.sv``) and + against the field packing in ``asm_v2.py``'s ``Waveform.compile()`` + (``env`` is 3 bytes, ``gain`` is 4 bytes). The ``tprocv2_assembler.py`` + ``Alias_List`` (``w_env`` -> ``w2``, ``w_gain`` -> ``w3``) is correct as + written; there is no assembler bug. + +.. contents:: Table of Contents + :depth: 3 + +------------------------------------------------------------------------------- + +PREFACE: How to use this manual +=============================== + +This manual is organized by **task**, not just by instruction. If you know what you want to *do* (e.g., "wait for ADC data", "generate a chirp", "synchronize two boards"), look for the **task‑based section** (Chapter 9). If you need the exact encoding of an instruction, see the **reference section** (Chapter 11). + +**Conventions used in this manual:** + +.. list-table:: + :header-rows: 1 + + * - Symbol + - Meaning + * - ``[value]`` + - Optional parameter + * - ``#123`` + - Immediate (literal) value + * - ``&123`` + - Memory address + * - ``@123`` + - Time value + * - ``p0 .. p15`` + - Port number + * - ``// text`` + - Comment + * - ``> text`` + - Explanation of the preceding line + +------------------------------------------------------------------------------- + +.. _tproc-wis: + +1. What is the qick_processor (tProcV2)? (Executive Summary) +============================================================ + +The tProcessor is a **hard real‑time co‑processor** inside the QICK FPGA. It runs a user‑written program that controls waveform generation, data acquisition, and feedback with **nanosecond precision**. + +**What it does well:** + +* Generate sequences of pulses with precise timing +* React to ADC data within microseconds +* Coordinate multiple DAC/ADC channels +* Run independently of the Linux operating system + +**What it does NOT do:** + +* Floating‑point math (use fixed‑point or Python pre‑computation) +* Complex control flow (stay mostly linear with short loops) +* Interact with files or networks (that is the ARM's job) + +**Typical experiment flow:** + +:: + + +-------------+ +-------------+ +-------------+ +-------------+ + | Initialize | --> | Configure | --> | Run tProc | --> | Read back | + | board, DACs | | waveforms | | program | | results | + +-------------+ +-------------+ +-------------+ +-------------+ + ^ | | | + | v v v + Python (PS) Python/ARM tProcessor Python/ARM + (once) (pre‑compute) (real‑time) (post‑process) + +------------------------------------------------------------------------------- + +Overview +-------- + +The ``qick_processor`` (tProcV2) is a custom 32-bit data, 72-bit instruction processor +designed specifically for generating precisely timed waveforms, handling data, and +triggering events in quantum computing control systems. + +Architecture +------------ + +The processor is composed of two main blocks: + +- **CORE**: Contains the Processing Unit (``CORE_CPU``) and the Memory Unit (``CORE_MEM``). + The ``CORE_CPU`` is a 5-stage pipelined Harvard architecture executing one instruction per + clock cycle, with 18 instructions optimized for multi-operation execution. It includes a + PC-Stack supporting up to 256 nested ``CALL`` functions and a Pseudo Random Number Generator + (LFSR) for 32-bit pseudorandom number generation. + +- **DISPATCHER**: Responsible for timely port signal output. It manages three FIFOs (Wave, + Trigger, and Data), each holding specific information for output along with a designated + time. The Dispatcher continuously compares the current time against the scheduled time in + each FIFO, updating the corresponding port when the scheduled time is reached. + +Memory +------ + +``CORE_MEM`` consists of three distinct memory components: + +- **Program Memory (PMEM)**: 72-bit memory for storing instructions. +- **Data Memory (DMEM)**: 32-bit memory for user data storage. +- **WaveParam Memory (WMEM)**: 32-bit memory for storing waveform parameters to be written + to the Analog Wave Ports. + +Output Ports +------------ + +The ``qick_processor`` provides three groups of output ports: + +- **Four** 32-bit Data Ports (DPORT) +- **Thirty-two** Trigger Output Ports (TRIG) +- **Sixteen** Analog Wave Ports (WPORT) + +Special Features +---------------- + +- **Multiplication Unit (FPGA DSP)**: Performs the operation ``(D±A)*B±C`` in 2 clock cycles. +- **Division Unit (Custom)**: Provides the quotient and remainder of an integer division in + 32 clock cycles. +- **Pseudo Random Number Generator (LFSR)**: Configurable Linear Feedback Shift Register for + 32-bit pseudorandom number generation. +- **Nested CALL**: Supports function call nesting up to 256 levels. +- **Debugging Capabilities**: Step-by-step execution, time stepping, core stepping, status + reading, and debug signal output. + +AXI Interface +------------- + +The AXI stream interface is monitored by a block (``DPORT_IN Register``) that updates on +every new data reception, storing it and updating a status bit in the SREG +(``PROC_xREG``). The ``qick_processor`` exposes a set of AXI Registers (``PROC_xREG``) +that can be read and written through the AXI-Lite interface. + +Compatibility +------------- + +tProcV2 is compatible with Signal Generators of types **INT**, **Mux**, and **SGV6** +for waveform generation and output. + +------------------------------------------------------------------------------- + +.. _tproc-quick-ref: + +2. Quick Reference Card +======================= + +**Most common instructions (keep this nearby):** + +.. code-block:: text + + // Register operations + REG_WR rd imm #value // rd = value + REG_WR rd op -op(rs + #imm) // rd = rs + imm + REG_WR rd op -op(rs1 + rs2) // rd = rs1 + rs2 + + // Memory operations + REG_WR rd dmem [&addr] // rd = DMEM[addr] + DMEM_WR [&addr] imm #value // DMEM[addr] = value + + // Waveform operations + REG_WR r_wave wmem [&addr] // load waveform from WMEM + WPORT_WR pN r_wave @time // output waveform + + // Port operations + DPROT_WR pN imm value @time // output 32‑bit data + DPROT_WR pN reg rX @time // output from register + TRIG pN set @time // set trigger high + TRIG pN clr @time // set trigger low + DPORT_RD pN // read input port (to s_port_l/h) + + // Flow control + JUMP LABEL -if(condition) // conditional branch + CALL SUBROUTINE + RET + + // Waiting + WAIT time @N // wait until current time >= N + WAIT div_dt // wait for division to complete + WAIT port_dt // wait for input port data + // there is no "WAIT arith_dt" macro (unlike div_dt/port_dt/time, the + // assembler doesn't recognize it) -- poll manually, see 5.10.3 below: + TEST -op(s_status AND #h0002) -uf + JUMP HERE -if(Z) // loop while ARITH_DT_NEW (bit 1) is 0 + + // Peripherals + DIV num den // start division (32 cycles) + ARITH T rA rB // rA * rB (2 cycles) + +**Condition codes (most useful):** + +.. code-block:: text + + -if(Z) // zero (result == 0) + -if(NZ) // non‑zero (result != 0) ← most common for loops + -if(S) // negative (result < 0) + -if(NS) // non‑negative (result >= 0) + -if(F) // flag set (internal or external) + -if(NF) // flag clear + +.. list-table:: Condition Codes (bits 9:7 of the code field) + :header-rows: 1 + :widths: 12 10 18 60 + + * - Code + - Mnemonic + - Tested + - Description + * - 000 + - ``1`` (always) + - - + - Always execute + * - 001 + - ``Z`` + - ALU Z flag + - Execute if last ALU/ALU-like result was zero + * - 010 + - ``S`` + - ALU S flag + - Execute if last result was negative (sign bit set) + * - 011 + - ``NZ`` + - not(Z) + - Execute if last result was non-zero + * - 100 + - ``NS`` + - not(S) + - Execute if last result was non-negative + * - 101 + - ``F`` + - external flag_i + - Execute if flag selected by ``s_cfg.flag_src`` is high + * - 110 + - ``NF`` + - not(flag_i) + - Execute if flag is low + * - 111 + - (RFU) + - - + - Reserved for future use + +The condition codes are encoded in bits ``[9:7]`` of the 16-bit ``code`` +field (which itself is the high half of the 72-bit instruction word). + +.. list-table:: WAIT Options + :header-rows: 1 + :widths: 20 40 40 + + * - WAIT Option + - Condition Tested + - Expands To + * - ``WAIT time @N`` + - ``s_usr_time >= N`` + - TEST + JUMP loop + * - ``WAIT div_dt`` + - ``DIV_DT_NEW`` (s_status[3] == 1) + - TEST + JUMP loop + * - *(no macro)* -- ``ARITH_DT_NEW`` + - ``ARITH_DT_NEW`` (s_status[1] == 1) + - **Not a recognized WAIT macro** -- the assembler raises an error on + ``WAIT arith_dt``, unlike the other rows here. Write the TEST + JUMP + loop by hand (see :ref:`5.10.3 ` below). + * - ``WAIT port_dt`` + - Any ``PORT_DT_NEW`` bit set + - TEST + JUMP loop + +.. list-table:: CLEAR Options + :header-rows: 1 + :widths: 25 25 50 + + * - CLEAR Option + - s_ctrl Bit + - Clears + * - ``CLEAR arith`` + - 0 + - ``ARITH_DT_NEW`` + * - ``CLEAR div`` + - 1 + - ``DIV_DT_NEW`` + * - ``CLEAR qnet`` + - 2 + - ``QNET_DT_NEW`` + * - ``CLEAR qcom`` + - 3 + - ``QCOM_DT_NEW`` + * - ``CLEAR qpa`` + - 4 + - ``QPA_DT_NEW`` + * - ``CLEAR qpb`` + - 5 + - ``QPB_DT_NEW`` + * - ``CLEAR port`` + - 6 + - All ``PORT_DT_NEW`` bits + +Complete Instruction Header Reference +------------------------------------- + +.. list-table:: Instruction Headers (bits 71:69) + :header-rows: 1 + :widths: 12 28 22 42 + + * - Header + - Instruction Category + - Instructions + - Section + * - 000 + - Configuration + - NOP, TEST + - 5.15 + * - 001 + - Branch + - JUMP, CALL, RET + - 5.16 + * - 010 + - Internal Peripheral Control + - TIME, FLAG, ARITH, DIV + - 5.8 + * - 011 + - External Peripheral Control + - NET, COM, PA, PB + - 5.8 + * - 100 + - Register Write + - REG_WR + - 5.14 + * - 101 + - Memory Write + - DMEM_WR, WMEM_WR + - 5.13 + * - 110 + - Port I/O + - DPORT_WR, TRIG, WPORT_WR, DPORT_RD + - 5.12 + * - 111 + - Reserved + - - + - - + +------------------------------------------------------------------------------- + +3. Architecture Deep Dive +========================= + +3.1. Block Diagram with Data Flow +--------------------------------- + +The diagram below shows tProcessor v2's top-level composition, verified +directly against ``tprocv2.sv``'s own instantiations: the AXI-Lite register +bank feeds the start/stop/time-base control and the PS memory-DMA path; +``qproc_core`` (the 5-stage pipeline this chapter covers instruction by +instruction, plus its program/data/wave memories) drives the +``qproc_dispatcher``, which is the actual mechanism behind ``WPORT_WR``/ +``TRIG``/``DPORT_WR`` reaching the outside world -- each gets its own +async FIFO plus a time comparator against the running ``t_clk`` time base +(see Section 3.3, *FIFO Depth and Flow Control*, below for the FIFO side +of this). + +.. figure:: images/firmware/tprocv2-blocks.svg + :align: center + :width: 95% + + Top-level block diagram of ``tprocv2.sv``. + +3.2. Clock Domain Crossing (CDC) Explained +------------------------------------------- + +Because the core and the dispatcher run on different clocks, any communication between them must cross clock domains. This introduces **latency** and **metastability protection**. + +**Critical paths and their latencies:** + +.. list-table:: + :header-rows: 1 + + * - Action + - From domain + - To domain + - Latency (cycles of destination clock) + * - Write to FIFO (DPORT_WR) + - c_clk + - t_clk + - 2‑3 cycles + * - FIFO pop to output pin + - t_clk + - t_clk (same) + - 1 cycle + * - Read input port (DPORT_RD) + - t_clk (ADC) + - c_clk + - 2‑3 cycles + * - TIME instruction effect + - c_clk + - t_clk + - 3‑5 cycles + * - FLAG from Python to core + - ps_clk + - c_clk + - several µs (not for tight timing) + +**Practical implication:** Do not back‑to‑back TIME instructions without at least 5 c_clk cycles between them. + +**proc_start / proc_stop CDC:** In the packaged IP top (`qick_processor.sv`), `proc_start_i` and `proc_stop_i` are synchronized from `ps_clk_i` into `c_clk_i` via `cdc_reg_sync` and then edge-detected into one-shot pulses before reaching `tprocv2`. This mirrors the pulse-from-level handling in `qproc_ctrl` for `PS_TPROC_CTRL`. + +3.3. FIFO Depth and Flow Control +-------------------------------- + +The dispatcher has three independent FIFOs: + +.. list-table:: + :header-rows: 1 + + * - FIFO + - Entry width + - Default depth + - Configurable up to + - When is it written? + * - Wave FIFO (WFIFO) + - 168 bits + 48 bits time + - 8 + - 512 + - WPORT_WR + * - Data FIFO (DFIFO) + - 32 bits + 48 bits time + - 8 + - 512 + - DPORT_WR + * - Trigger FIFO (TFIFO) + - 1 bit + 48 bits time + - 8 + - 512 + - TRIG + +**Flow control behavior:** + +When a FIFO becomes full: + +- If ``tproc_cfg[10]`` (``DISABLE_FIFO_FULL_PAUSE``) = 0 (default): The core **stalls** (pauses) until space is available. +- If = 1: The core continues, but writes to the full FIFO are **dropped** (data loss). + +**Checklist for avoiding FIFO overflow:** + +- [ ] Count how many port writes you have in the shortest time window. +- [ ] Ensure that number ≤ FIFO depth. +- [ ] If you need more, increase FIFO depth in the hardware configuration (Chapter 10). +- [ ] Or add ``WAIT fifo_not_full`` (macro) before writes. + +3.4. Pipeline Stages and Hazards +-------------------------------- + +The tProcessor uses a 5-stage pipeline. Understanding it helps avoid performance issues. + +**Pipeline Stages:** + +The tProcessor uses a 5-stage pipeline: + +.. code-block:: text + + Cycle: 1 2 3 4 5 + ──────────────────────────────────── + Inst1: FETCH DECODE READ EXEC WRITE + Inst2: FETCH DECODE READ EXEC + Inst3: FETCH DECODE READ + Inst4: FETCH DECODE + Inst5: FETCH + + +**Branch Penalty:** + +When a branch is taken, the pipeline flushes, causing a **2-cycle penalty**. + +**Load-Use Hazard:** + +Reading a register immediately after writing may cause a 1-cycle stall. + +**Dual Task Hazard Prevention:** + +In dual task instructions, both operations read **original** register values. +Writes happen at the end of the cycle - no hazard. + +3.5. Interrupts (Important Limitation) +-------------------------------------- + +.. warning:: + The tProcessor v2 **does NOT support interrupts**. + +This is intentional for deterministic real-time behavior. + +**Alternatives for event response:** + +- Use ``WAIT`` macro (polls status register) +- Use short loops and check flags frequently +- Python can set external flags that tProc polls + + +------------------------------------------------------------------------------- + +.. _tproc-registers: + +4. Complete Register Bank Reference +==================================== + +The tProcessor has three independent register banks. Each bank has a different purpose and access method. + +4.1. Register Organization Overview +------------------------------------ + +The diagram below shows the physical organization of all registers: + +:: + + ╔══════════════════════════════════════════════════════════════════════════════╗ + ║ REGISTER BANKS (total 32+16+6 registers) ║ + ╠══════════════════════════════════════════════════════════════════════════════╣ + ║ ║ + ║ ┌─────────────────────────────┐ ┌──────────────────────────────┐ ║ + ║ │ DREG (Data Registers) │ │ SREG (Special Function) │ ║ + ║ │ ┌─────┬─────────────┐ │ │ ┌─────┬─────────────────┐ │ ║ + ║ │ │ r0 │ general │ │ │ │ s0 │ s_zero (always0)│ │ ║ + ║ │ │ r1 │ purpose │ │ │ │ s1 │ s_rand (LFSR) │ │ ║ + ║ │ │ r2 │ 32-bit │ │ │ │ s2 │ s_cfg / s_ctrl │ │ ║ + ║ │ │ ... │ registers │ │ │ │ s3 │ s_arith_low │ │ ║ + ║ │ │ r31 │ │ │ │ │ s4 │ s_div_q │ │ ║ + ║ │ └─────┴─────────────┘ │ │ │ s5 │ s_div_r │ │ ║ + ║ │ │ │ │ s6 │ s_core_r1 │ │ ║ + ║ │ Access: REG_WR rd ... │ │ │ s7 │ s_core_r2 │ │ ║ + ║ │ Example: REG_WR r5 imm #10 │ │ │ s8 │ s_port_l │ │ ║ + ║ └─────────────────────────────┘ │ │ s9 │ s_port_h │ │ ║ + ║ │ │ s10 │ s_status │ │ ║ + ║ ┌─────────────────────────────┐ │ │ s11 │ s_usr_time │ │ ║ + ║ │ WREG (Wave Param Registers)│ │ │ s12 │ s_core_w1 │ │ ║ + ║ │ ┌─────────┬──────────────┐ │ │ │ s13 │ s_core_w2 │ │ ║ + ║ │ │ w0 │ w_freq (32b)│ │ │ │ s14 │ s_out_time │ │ ║ + ║ │ │ w1 │ w_phase (32b)│ │ │ │ s15 │ s_addr │ │ ║ + ║ │ │ w2 │ w_env (24b)│ │ │ └─────┴─────────────────┘ │ ║ + ║ │ │ w3 │ w_gain (32b)│ │ │ │ ║ + ║ │ │ w4 │ w_length(32b)│ │ │ Access: REG_WR sX ... │ ║ + ║ │ │ w5 │ w_conf (16b)│ │ │ Example: REG_WR s10 op -op()│ ║ + ║ │ └─────────┴──────────────┘ │ └──────────────────────────────┘ ║ + ║ │ │ ║ + ║ │ These 6 registers are │ ║ + ║ │ concatenated to form │ ║ + ║ │ the 168-bit r_wave bus: │ ║ + ║ │ │ ║ + ║ │ r_wave = {w5, w4, w3, │ ║ + ║ │ w2, w1, w0} │ ║ + ║ │ │ ║ + ║ │ Access: REG_WR wX ... │ ║ + ║ │ Example: REG_WR w_freq imm │ ║ + ║ └─────────────────────────────┘ ║ + ║ ║ + ╚══════════════════════════════════════════════════════════════════════════════╝ + + +4.2. Physical Address Map +------------------------- + +Each register has a physical address used internally. You don't need this for assembly +programming (use names like ``r5``, ``s10``, ``w_freq``), but it's useful for debugging +or when reading the AXI debug registers. + +.. list-table:: Register Physical Addresses + :header-rows: 1 + :widths: 15 15 20 50 + + * - Bank + - Address Range + - Number + - Assembly Names + * - SREG + - 0x00 - 0x0F + - 16 + - ``s0`` ... ``s15`` (plus aliases) + * - DREG + - 0x10 - 0x1F + - 32 + - ``r0`` ... ``r31`` + * - WREG (wave params) + - 0x20 - 0x25 + - 6 + - ``w0`` (w_freq), ``w1`` (w_phase), ``w2`` (w_env), ``w3`` (w_gain), ``w4`` (w_length), ``w5`` (w_conf) + * - Special (r_wave) + - (virtual) + - 1 + - ``r_wave`` (168-bit concatenation of all wregs) + +4.3. Complete Register Table with Aliases +------------------------------------------ + +**DREG - Data Registers (r0 - r31)** + +These are general purpose. No predefined aliases, but you can create your own: + +.. code-block:: text + + .ALIAS loop_counter r0 + .ALIAS temp_result r1 + .ALIAS dmem_ptr r2 + +**SREG - Special Function Registers (s0 - s15)** + +.. list-table:: + :header-rows: 1 + :widths: 10 25 15 50 + + * - Address + - Assembly Name + - Aliases + - Description / Typical Use + * - 0x00 + - ``s0`` + - ``s_zero`` + - Always reads as 0. Useful for clearing or as a source. + * - 0x01 + - ``s1`` + - ``s_rand`` + - Pseudo-random number from LFSR. Configurable behavior via core_cfg. + * - 0x02 + - ``s2`` + - ``s_cfg`` (lower byte), ``s_ctrl`` (upper byte, one-shot) + - Configuration: data sources, flag sources. ``s_ctrl`` self-clears the cycle after it is written (one-shot). + * - 0x03 + - ``s3`` + - ``s_arith_low`` + - Lower 32 bits of ARITH (multiply) result. + * - 0x04 + - ``s4`` + - ``s_div_q`` + - Quotient from DIV operation. + * - 0x05 + - ``s5`` + - ``s_div_r`` + - Remainder from DIV operation. + * - 0x06 + - ``s6`` + - ``s_core_r1`` + - First data word from selected source (see s_cfg DT_SRC). + * - 0x07 + - ``s7`` + - ``s_core_r2`` + - Second data word from selected source. + * - 0x08 + - ``s8`` + - ``s_port_l`` + - Lower 32 bits of input port read (DPORT_RD). + * - 0x09 + - ``s9`` + - ``s_port_h`` + - Upper 32 bits of input port read (DPORT_RD). + * - 0x0A + - ``s10`` + - ``s_status`` + - Status flags: peripherals ready, new data, FIFO state. + * - 0x0B + - ``s11`` + - ``s_usr_time``, ``curr_usr_time`` + - Current user time (read-only). t_abs - t_ref. + * - 0x0C + - ``s12`` + - ``s_core_w1`` + - First data word written from PS (ARM) to core. + * - 0x0D + - ``s13`` + - ``s_core_w2`` + - Second data word written from PS to core. + * - 0x0E + - ``s14`` + - ``s_out_time``, ``out_usr_time`` + - Output time register for port writes (read/write, 32-bit). + * - 0x0F + - ``s15`` + - ``s_addr`` + - Address register for JUMP instructions. + +**Note on addressing:** the register page is selected by ``w_addr_i[6:5]``. ``2'b00`` selects the SREG page (addresses ``0x00..0x0F``), ``2'b01`` selects the DREG page (``0x10..0x1F``), ``2'b10`` selects the WREG page (``0x20..0x25``). Assembly names map ``s0..s15`` -> ``0x00..0x0F``, ``r0..r31`` -> ``0x10..0x2F``, ``w0..w5`` -> ``0x20..0x25``. + +**WREG - Wave Parameter Registers (w0 - w5)** + +These six registers are **individually accessible** but also concatenated into ``r_wave``. + +.. list-table:: + :header-rows: 1 + :widths: 10 15 15 15 45 + + * - Address + - Base Name + - Alias + - Bits + - Description + * - 0x20 + - ``w0`` + - ``w_freq`` + - 32 + - Frequency control word for DDS. 0 = DC, 2^31 = Nyquist. + * - 0x21 + - ``w1`` + - ``w_phase`` + - 32 + - Phase offset. 2^32 = 360 degrees. + * - 0x22 + - ``w2`` + - ``w_env`` + - 24 + - Starting address in envelope table memory. + * - 0x23 + - ``w3`` + - ``w_gain`` + - 32 + - Amplitude scaling. Signed 32-bit integer. + * - 0x24 + - ``w4`` + - ``w_length`` + - 32 + - Envelope length in samples. + * - 0x25 + - ``w5`` + - ``w_conf`` + - 16 + - Configuration bits for signal generator. + +**Special Composite: r_wave** + +.. code-block:: text + + r_wave is not a physical register. It is a 168-bit bus formed by concatenation: + + r_wave = { w5 (w_conf), w4 (w_length), w3 (w_gain), + w2 (w_env), w1 (w_phase), w0 (w_freq) } + + Bits: 167:152 = w_conf (16 bits, w5) + 151:120 = w_length (32 bits, w4) + 119:88 = w_gain (32 bits, w3) + 87:64 = w_env (24 bits, w2[23:0]) + 63:32 = w_phase (32 bits, w1) + 31:0 = w_freq (32 bits, w0) + + You can write to ``r_wave`` using ``REG_WR r_wave ...``, which updates **all six** wave parameters at once. + +4.4. Access Examples by Register Type +-------------------------------------- + +**Writing to DREG (data registers):** + +.. code-block:: text + + REG_WR r5 imm #100 // r5 = 100 + REG_WR r5 op -op(r0 + r1) // r5 = r0 + r1 + REG_WR r5 dmem [&10] // r5 = DMEM[10] + +**Reading from DREG (as source):** + +.. code-block:: text + + REG_WR r6 op -op(r5) // r6 = r5 (copy) + DMEM_WR [&20] imm r5 // DMEM[20] = r5 + DPROT_WR p0 reg r5 @100 // output r5 to port + +**Writing to SREG:** + +.. code-block:: text + + REG_WR s_cfg imm src_arith // configure data source + REG_WR s_out_time imm #1000 // set output time + REG_WR s_addr label LOOP // store jump address + +**Reading from SREG:** + +.. code-block:: text + + REG_WR r0 op -op(s_rand) // r0 = random number + REG_WR r1 op -op(s_usr_time) // r1 = current time + REG_WR r2 op -op(s_status) // r2 = status bits + +**Writing to WREG (individual parameter):** + +.. code-block:: text + + REG_WR w_freq imm #0x1000000 // set frequency + REG_WR w_gain imm #32768 // set gain + REG_WR w_length imm #1000 // set length + +**Writing to r_wave (all parameters at once):** + +.. code-block:: text + + REG_WR r_wave wmem [&5] // load from WMEM + // or + REG_WR r_wave imm #0x... // immediate (not practical, too large) + +**Using wreg as source in ALU operations:** + +.. code-block:: text + + REG_WR r0 op -op(w_freq) // r0 = current frequency + REG_WR r1 op -op(w_freq + #100) // r1 = frequency + 100 + +4.5. Flag Register (not memory-mapped) +-------------------------------------- + +The tProcessor has an **internal flag** (IF) and can also use an **external flag** (EF) set from Python. + +.. list-table:: + :header-rows: 1 + + * - Flag + - Set by + - Cleared by + - Test with + * - Internal Flag (IF) + - ``FLAG set``, or automatically by peripherals + - ``FLAG clr`` + - ``-if(F)``, ``-if(NF)`` + * - External Flag (EF) + - Python (tproc_ctrl bit 13) + - Python (tproc_ctrl bit 14) + - ``-if(F)``, ``-if(NF)`` (if configured) + +Which flag is used by ``-if(F)`` is determined by ``s_cfg[7:4]`` (FLAG_SRC). + +4.6. Detailed Bit Field Definitions +------------------------------------ +.. _s_cfg_details: + +s_cfg - Configuration Register (Lower 8 bits of s2) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This register controls data routing and flag selection. Write using ``REG_WR s_cfg imm ``. + +**Bit Fields:** + +.. list-table:: + :header-rows: 1 + :widths: 10 15 15 60 + + * - Bits + - Field + - Assembler Aliases + - Description + * - 3:0 + - ``DT_SRC`` + - ``src_axi``, ``src_arith``, ``src_qnet``, etc. + - Selects source for ``s_core_r1`` (s6) and ``s_core_r2`` (s7). + + * 0x0 (``src_axi``): Reads Python-written data (``TPROC_W_DT1/2``). + * 0x1 (``src_arith``): Reads ARITH result (lower 32 bits). + * 0x2 (``src_qnet``): Reads QNET input data. + * 0x3 (``src_qcom``): Reads QCOM input data. + * 0x4 (``src_qpa``): Reads QP1 input data. + * 0x5 (``src_qpb``): Reads QP2 input data. + * 0x6 (``src_core``): Reads the other core's ``CORE_W_DT``. + * 0x7 (``src_port``): Reads the input port data (``DPORT_RD``). + * - 7:4 + - ``FLAG_SRC`` + - ``flg_int``, ``flg_axi``, ``flg_ext``, ``flg_div``, etc. + - Selects the flag source for conditional execution ``-if(F)``. + + * 0x0 (``flg_int``): Internal Flag. + * 0x1 (``flg_axi``): AXI Flag (set/cleared by PS via ``tproc_ctrl``). + * 0x2 (``flg_ext``): External Flag (``ext_flag_i``). + * 0x3 (``flg_div`` / ``flg_arith``): ``ARITH_DT_NEW | DIV_DT_NEW``. + * 0x4 (``flg_port``): Any ``PORT_DT_NEW`` bit set. + * 0x5 (``flg_qnet``): QNET ``flag_i``. + * 0x6 (``flg_qcom``): QCOM ``flag_i``. + * 0x7 (``flg_qpa``): QP1 ``flag_i``. + +s_ctrl - Clear Flags Register (Upper 8 bits of s2) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Write **'1'** to clear the corresponding ``_NEW`` flag in ``s_status``. The bit self-clears the cycle after it is written (one-shot). + +**Bit Fields:** + +.. list-table:: + :header-rows: 1 + :widths: 10 15 15 60 + + * - Bits (in byte) + - Field + - Assembler Aliases + - Description + * - 0 + - ``arith_clr`` + - ``clr_arith`` + - Clears ``ARITH_DT_NEW`` (bit 1 of ``s_status``). + * - 1 + - ``div_clr`` + - ``clr_div`` + - Clears ``DIV_DT_NEW`` (bit 3 of ``s_status``). + * - 2 + - ``qnet_clr`` + - ``clr_qnet`` + - Clears ``QNET_DT_NEW`` (bit 5 of ``s_status``). + * - 3 + - ``qcom_clr`` + - ``clr_qcom`` + - Clears ``QCOM_DT_NEW`` (bit 7 of ``s_status``). + * - 4 + - ``qpa_clr`` + - ``clr_qpa`` + - Clears ``QPA_DT_NEW`` (bit 9 of ``s_status``). + * - 5 + - ``qpb_clr`` + - ``clr_qpb`` + - Clears ``QPB_DT_NEW`` (bit 11 of ``s_status``). + * - 6 + - ``port_clr`` + - ``clr_port`` + - Clears all ``PORT_DT_NEW`` bits (bits 31:16 of ``s_status``). + +s_status - Status Register (Read-Only, s10) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Bit Fields:** + +.. list-table:: + :header-rows: 1 + :widths: 10 15 15 60 + + * - Bit + - Name + - Assembler Mask + - Description + * - 0 + - ``ARITH_RDY`` + - ``#1`` + - Arithmetic unit is ready/idle (high when not working). + * - 1 + - ``ARITH_DT_NEW`` + - ``#2`` + - New arithmetic result available. Clear via ``CLEAR arith`` or reading ``s_arith_low``. + * - 2 + - ``DIV_RDY`` + - ``#4`` + - Divider unit is ready. + * - 3 + - ``DIV_DT_NEW`` + - ``#8`` + - New division result available. Clear via ``CLEAR div`` or reading ``s_div_q``/``s_div_r``. + * - 4 + - ``QNET_RDY`` + - ``#0x10`` + - QNET peripheral is ready for a new transaction. + * - 5 + - ``QNET_DT_NEW`` + - ``#0x20`` + - New data from QNET received. Clear via ``CLEAR qnet`` or reading ``s_core_r1``/``s_core_r2`` with ``DT_SRC=src_qnet``. + * - 6 + - ``QCOM_RDY`` + - ``#0x40`` + - QCOM peripheral is ready. + * - 7 + - ``QCOM_DT_NEW`` + - ``#0x80`` + - New data from QCOM received. Clear via ``CLEAR qcom``. + * - 8 + - ``QPA_RDY`` + - ``#0x100`` + - QP1 peripheral is ready. + * - 9 + - ``QPA_DT_NEW`` + - ``#0x200`` + - New data from QP1 received. Clear via ``CLEAR qpa``. + * - 10 + - ``QPB_RDY`` + - ``#0x400`` + - QP2 peripheral is ready. + * - 11 + - ``QPB_DT_NEW`` + - ``#0x800`` + - New data from QP2 received. Clear via ``CLEAR qpb``. + * - 12 + - ``RFU`` + - ``#0x1000`` + - Reserved (always 0). + * - 13 + - ``RFU`` + - ``#0x2000`` + - Reserved (always 0). + * - 14 + - ``FIFO_FULL`` + - ``#0x4000`` + - At least one dispatcher FIFO is full. + * - 15 + - ``ANY_PORT_NEW`` + - ``#0x8000`` + - Any input port has new data (OR of ``PORT_DT_NEW[15:0]``). + * - 31:16 + - ``PORT_DT_NEW`` + - ``#0xFFFF0000`` + - **Bit mask per input port**. Bit 16 corresponds to Port 0. Set when new data arrives via ``DPORT_RD``. Clear via ``CLEAR port``. + +.. _core_cfg_details: + +core_cfg - LFSR Configuration (AXI Register 0x07) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Controls the behavior of the Pseudo-Random Number Generator (LFSR) connected to ``s_rand``. + +**Bit Fields:** + +.. list-table:: + :header-rows: 1 + :widths: 10 25 60 + + * - Bits + - Value (CFG) + - Description + * - 1:0 + - ``0`` (STOP) + - LFSR stops. The value in ``s_rand`` remains constant. + * - 1:0 + - ``1`` (FREE_RUN) + - LFSR advances **every** `c_clk` cycle. Fastest random updates. + * - 1:0 + - ``2`` (READ_STEP) + - LFSR advances **only when** the core reads the ``s_rand`` register. + * - 1:0 + - ``3`` (WRITE_STEP) + - LFSR advances **only when** the core writes to the ``s_rand`` register (value is discarded). + +4.7. Peripherals Control & Status Registers +-------------------------------------------- + +Each peripheral (ARITH, DIV, TIME, FLAG) has a specific control and status interface. +The table below summarizes the registers involved: + +.. list-table:: Peripheral Register Map + :header-rows: 1 + :widths: 30 30 30 35 + + * - Peripheral + - Control Register + - Status Register + - Data Register + * - ARITH (Multiply) + - Instruction only (no control reg) + - ``s_status[0]`` (RDY), ``s_status[1]`` (NEW) + - ``s_arith_low`` (s3) + * - DIV (Divide) + - Instruction only + - ``s_status[2]`` (RDY), ``s_status[3]`` (NEW) + - ``s_div_q`` (s4), ``s_div_r`` (s5) + * - TIME (Time control) + - ``TIME`` instruction + - (none) + - ``s_usr_time`` (s11), ``s_out_time`` (s14) + * - FLAG (Internal) + - ``FLAG`` instruction + - (none) + - (none - just the flag bit) + * - LFSR (Random) + - ``core_cfg`` (AXI) + - (none) + - ``s_rand`` (s1) + +4.7.1. ARITH (Multiply-Accumulate) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ARITH peripheral is started by an instruction, not by writing a register. + +**Control Flow:** + +.. code-block:: text + + // 1. Start operation + ARITH PTP r1 r2 r3 r4 // (r1+r2)*r3+r4 + + // 2. Wait for completion (poll s_status -- no WAIT macro for this, see 5.10.3) + TEST -op(s_status AND #h0002) -uf + JUMP HERE -if(Z) // loops until bit 1 (ARITH_DT_NEW) is set + + // 3. Read result + REG_WR r5 op -op(s_arith_low) + + // 4. Clear new-data flag (optional) + CLEAR arith // macro: writes s_ctrl[16]=1 + +**Status Bits (s_status):** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Bit + - Name + - Description + * - 0 + - ``ARITH_RDY`` + - Set when ARITH is idle. Cleared when operation starts. + * - 1 + - ``ARITH_DT_NEW`` + - Set when result is ready. Cleared by reading or ``CLEAR arith``. + +**Data Register:** + +- ``s_arith_low`` (s3): Lower 32 bits of the 64-bit result. +- The upper 32 bits are NOT accessible in this version. + +**Control Register: None** - The ARITH unit has no memory-mapped control register. +All configuration is done via the instruction opcode. + +4.7.2. DIV (Integer Division) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Control Flow:** + +.. code-block:: text + + // 1. Start division + DIV r1 r2 // r1 / r2 (unsigned) + + // 2. Do other work while division runs (32 cycles) + REG_WR r3 op -op(r3 + #1) + + // 3. Wait for completion + WAIT div_dt + + // 4. Read results + REG_WR r4 op -op(s_div_q) // quotient + REG_WR r5 op -op(s_div_r) // remainder + + // 5. Clear flag + CLEAR div + +**Status Bits (s_status):** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Bit + - Name + - Description + * - 2 + - ``DIV_RDY`` + - Set when DIV is idle. Cleared when division starts. + * - 3 + - ``DIV_DT_NEW`` + - Set when result is ready. Cleared by reading or ``CLEAR div``. + +**Data Registers:** + +- ``s_div_q`` (s4): 32-bit quotient (unsigned). +- ``s_div_r`` (s5): 32-bit remainder (unsigned). + +**Important Notes:** + +- Division is **unsigned only**. For signed division, convert to positive values. +- Denominator cannot be zero (undefined behavior). +- Takes exactly 32 cycles to complete (plus pipeline overhead). + +4.7.3. TIME (Time Control Peripheral) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIME peripheral is controlled by dedicated instructions, not by memory-mapped registers. + +**Instructions:** + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Instruction + - Example + - Effect + * - ``TIME rst`` + - ``TIME rst`` + - ``t_abs = 0``, ``t_ref = 0``, reset core + * - ``TIME set_ref rX`` + - ``TIME set_ref r0`` + - ``t_ref = rX`` (t_abs unchanged) + * - ``TIME inc_ref #N`` + - ``TIME inc_ref #100`` + - ``t_ref = t_ref + N`` + * - ``TIME updt #N`` + - ``TIME updt #10`` + - ``t_abs = t_abs + N`` (rarely used) + +**Internal Time Registers (not directly accessible):** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Register + - Width + - Description + * - ``t_abs`` + - 48 bits + - Absolute time counter. Runs at DAC clock (`t_clk`). + * - ``t_ref`` + - 48 bits + - Reference time offset. Defines where user time = 0. + * - ``t_usr`` + - 32 bits (read via s11) + - User time = `t_abs - t_ref` (truncated to 32 bits). + +**Time-Related SREG Registers:** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 20 45 + + * - Register + - Alias + - Access + - Description + * - s11 + - ``s_usr_time`` + - Read-only + - Current user time (32 bits of `t_abs - t_ref`) + * - s14 + - ``s_out_time`` + - Read/Write + - Output time for next port write (signed 32-bit) + +**Clock Domain Crossing Warning:** + +The TIME instruction crosses from `c_clk` to `t_clk` domain. +Do **not** execute two TIME instructions with less than 5 cycles between them. + +4.7.4. FLAG (Internal Flag) +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The internal flag is a single bit that can be used for conditional execution. + +**Instructions:** + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Instruction + - Example + - Effect + * - ``FLAG set`` + - ``FLAG set`` + - Sets internal flag to 1. + * - ``FLAG clr`` + - ``FLAG clr`` + - Clears internal flag to 0. + * - ``FLAG inv`` + - ``FLAG inv`` + - Toggles internal flag (0→1, 1→0). + +**Usage with Conditional Execution:** + +.. code-block:: text + + FLAG set // set flag + REG_WR r0 imm #100 -if(F) // executed (flag=1) + FLAG clr + REG_WR r1 imm #200 -if(F) // NOT executed (flag=0=NF) + +**Note:** The flag used by `-if(F)` can be configured via `s_cfg[7:4]` (FLAG_SRC) to be: +- Internal flag (``flg_int``) +- External flag from Python (``flg_axi``) +- Division completion flag (``flg_div``) +- Etc. + +4.7.5. LFSR (Random Number Generator) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The LFSR generates pseudo-random numbers accessible via `s_rand` (s1). + +**Configuration Register (AXI core_cfg, address 0x07):** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Bits + - Value + - Mode + * - 1:0 + - 0 (``STOP``) + - LFSR stops. `s_rand` constant. + * - 1:0 + - 1 (``FREE_RUN``) + - LFSR advances every `c_clk` cycle. + * - 1:0 + - 2 (``READ_STEP``) + - LFSR advances when `s_rand` is read. + * - 1:0 + - 3 (``WRITE_STEP``) + - LFSR advances when `s_rand` is written. + +**Example Configuration (from Python):** + +.. code-block:: python + + # Configure LFSR to advance on each read (mode 2 = READ_STEP) + soc.tproc.set_lfsr_cfg(mode=2) + +**Assembly Usage:** + +.. code-block:: text + + REG_WR r0 op -op(s_rand) // reads random number (advances if mode=2) + REG_WR r1 op -op(s_rand) // next random number + +**Polynomial:** The LFSR implements Fibonacci series with polynomial +`x^31 + x^21 + x^1 + x^0` (maximum length sequence). + +4.7.6. WAIT and CLEAR Macros +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +These are not hardware peripherals but assembler macros that expand to instruction sequences. + +**WAIT time @N - Expansion:** + +.. code-block:: text + + // WAIT time @1000 expands to: + TEST -op(s_usr_time - #1000) + JUMP HERE -if(S) // loop until s_usr_time >= 1000 + +**Waiting for ARITH -- no macro, write it by hand:** + +Unlike ``time``/``div_dt``/``port_dt``, ``arith_dt`` is **not** a +recognized ``WAIT`` operand -- the assembler raises an error if you write +``WAIT arith_dt``. Write the equivalent TEST + JUMP loop directly: + +.. code-block:: text + + TEST -op(s_status AND #2) // test bit 1 (ARITH_DT_NEW) + JUMP HERE -if(Z) // loop until set + +**CLEAR arith - Expansion:** + +.. code-block:: text + + // CLEAR arith expands to: + REG_WR s_ctrl imm clr_arith // s_ctrl[16] = 1 + +4.7.7. Summary: Peripheral Control Flow Pattern +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Standard pattern for all peripherals:** + +.. code-block:: text + + // 1. Start (instruction only) + ARITH T r1 r2 + + // 2. Wait for completion (poll s_status -- no WAIT macro, write by hand) + TEST -op(s_status AND #2) -uf + JUMP HERE -if(Z) + + // 3. Read result (from dedicated sreg) + REG_WR r3 op -op(s_arith_low) + + // 4. Clear new-data flag + CLEAR arith + +**For peripherals that produce new data asynchronously (input ports):** + +.. code-block:: text + + // 1. Wait for data arrival (poll s_status) + WAIT port_dt + + // 2. Read data + DPORT_RD p0 + REG_WR r0 op -op(s_port_l) + + // 3. Clear flag (auto-cleared by read, or manually) + CLEAR port + +4.8. Quick Reference: Register Naming in Assembly +-------------------------------------------------- + +.. list-table:: + :header-rows: 1 + :widths: 20 30 50 + + * - Register Type + - Prefix + - Examples + * - Data registers + - ``r`` + - ``r0``, ``r1``, ... ``r31`` + * - Special functions + - ``s`` + - ``s0``, ``s_rand``, ``s_status``, ``s_out_time`` + * - Wave parameters + - ``w`` + - ``w0``, ``w_freq``, ``w_phase``, ``w_gain`` + * - Composite wave + - ``r_wave`` + - ``r_wave`` (no number) + * - Aliases (user-defined) + - any + - ``.ALIAS my_var r5`` then use ``my_var`` + +**Important:** The assembler is case‑sensitive for register names. ``R0`` is NOT the same as ``r0``. Always use lowercase ``r``, ``s``, ``w``. + +4.9. Common Mistakes with Registers +------------------------------------ + +**Mistake 1: Using r_wave when you meant a wreg** + +.. code-block:: text + + // Wrong: trying to write only frequency + REG_WR r_wave imm #0x1000000 // This would zero out other parameters! + + // Correct: write to w_freq directly + REG_WR w_freq imm #0x1000000 + +**Mistake 2: Reading s_status without masking** + +.. code-block:: text + + // Wrong: compares entire status (many bits may be set) + TEST -op(s_status - #4) // checks if status == 4 + + // Correct: mask only the bit you care about + TEST -op(s_status AND #4) // checks only bit 2 + +**Mistake 3: Using s_out_time as read‑only** + +.. code-block:: text + + // Wrong: trying to read current time + REG_WR r0 op -op(s_out_time) // this reads the OUTPUT time, not current time! + + // Correct: use s_usr_time for current time + REG_WR r0 op -op(s_usr_time) + +------------------------------------------------------------------------------- + +5. Instruction Set – Task‑Oriented Guide +======================================== + +5.1. Moving Data +---------------- + +**Register to register:** + +.. code-block:: text + + REG_WR r1 op -op(r0) // r1 = r0 + REG_WR r1 op -op(s_rand) // r1 = random number + +**Immediate to register:** + +.. code-block:: text + + REG_WR r0 imm #12345 // r0 = 12345 + REG_WR r0 imm #hDEADBEEF // r0 = 0xDEADBEEF (hex) + REG_WR r0 imm #b10101010 // r0 = 0xAA (binary) + +**Memory to register:** + +.. code-block:: text + + REG_WR r0 dmem [&100] // r0 = DMEM[100] + REG_WR r_wave wmem [&5] // r_wave = WMEM[5] + +**Register to memory:** + +.. code-block:: text + + DMEM_WR [&100] imm #42 // DMEM[100] = 42 + DMEM_WR [r0] op -op(r1) // DMEM[r0] = r1 + WMEM_WR [&5] // WMEM[5] = r_wave + +5.2. Arithmetic and Logic +------------------------- + +**Addition and subtraction:** + +.. code-block:: text + + REG_WR r2 op -op(r0 + #100) // r2 = r0 + 100 + REG_WR r2 op -op(r0 - r1) // r2 = r0 - r1 + +**Bitwise operations:** + +.. code-block:: text + + REG_WR r2 op -op(r0 AND #hFF) // r2 = r0 & 0xFF (lower byte) + REG_WR r2 op -op(r0 OR r1) // r2 = r0 | r1 + REG_WR r2 op -op(r0 XOR r1) // r2 = r0 ^ r1 + REG_WR r2 op -op(NOT r0) // r2 = ~r0 + +**Shifts:** + +.. code-block:: text + + REG_WR r2 op -op(r0 SL #2) // r2 = r0 << 2 (logical left) + REG_WR r2 op -op(r0 SR #1) // r2 = r0 >> 1 (logical right) + REG_WR r2 op -op(r0 ASR #1) // r2 = r0 >> 1 (arithmetic, sign preserved) + +**Special operations:** + +.. code-block:: text + + REG_WR r2 op -op(MSH r0) // r2 = upper 16 bits of r0 + REG_WR r2 op -op(LSH r0) // r2 = lower 16 bits of r0 + REG_WR r2 op -op(SWP r0) // r2 = swap upper and lower 16 bits + REG_WR r2 op -op(ABS r0) // r2 = |r0| (absolute value) + REG_WR r2 op -op(PAR r0) // r2 = parity of r0 (0 or 1) + +5.3. Multiplication (ARITH) +--------------------------- + +The ARITH unit uses the FPGA's DSP48 slices. It computes ``(A op D) * B [+|- C]`` +(the DSP48E2's pre-adder is on the A/D inputs, NOT on B; ``B`` is the +multiplier and ``C`` is the post-adder/subtracter). + +**Operation codes:** + +.. list-table:: + :header-rows: 1 + :widths: 18 30 52 + + * - Mnemonic + - Formula + - Example + * - ``ARITH T A B`` + - A * B + - ``ARITH T r1 r2`` = r1 * r2 + * - ``ARITH TP A B C`` + - A * B + C + - ``ARITH TP r1 r2 s_arith_l`` = r1 * r2 + s3 + * - ``ARITH TM A B C`` + - A * B - C + - ``ARITH TM r1 r2 r3`` = r1 * r2 - r3 + * - ``ARITH PT D A B`` + - (D + A) * B + - ``ARITH PT r1 r2 r3`` = (r1 + r2) * r3 + * - ``ARITH MT D A B`` + - (D - A) * B + - ``ARITH MT r1 r2 r3`` = (r1 - r2) * r3 + * - ``ARITH PTP D A B C`` + - (D + A) * B + C + - ``ARITH PTP r1 r2 r3 r4`` = (r1 + r2) * r3 + r4 + * - ``ARITH PTM D A B C`` + - (D + A) * B - C + - ``ARITH PTM r1 r2 r3 r4`` = (r1 + r2) * r3 - r4 + * - ``ARITH MTP D A B C`` + - (D - A) * B + C + - ``ARITH MTP r1 r2 r3 r4`` = (r1 - r2) * r3 + r4 + * - ``ARITH MTM D A B C`` + - (D - A) * B - C + - ``ARITH MTM r1 r2 r3 r4`` = (r1 - r2) * r3 - r4 + +.. note:: + The written argument order is **D, A, B[, C]** for the P/M variants (the + mnemonic's first operand is the pre-adder's ``D`` input, not ``A``) -- + verified against ``Instruction.ARITH()`` in ``tprocv2_assembler.py``, + which maps ``R1`` -> ``RsD``, ``R2`` -> ``RsA``, ``R3`` -> ``RsB``, and + (where present) ``R4`` -> ``RsC``. An earlier revision of this table had + A and D swapped in every worked example. + + +**Typical use: multiply‑accumulate for digital filtering** + +.. code-block:: text + + // Example: y = (r1 * r2) + r3 + REG_WR r1 imm #100 + REG_WR r2 imm #200 + REG_WR r3 imm #50 + ARITH T r1 r2 // r1 * r2 takes 2 cycles + TEST -op(s_status AND #2) -uf // no WAIT macro for arith_dt -- poll by hand + JUMP HERE -if(Z) + REG_WR r4 op -op(s_arith_low) // r4 = 20000 (product) + REG_WR r4 op -op(r4 + r3) // r4 = 20000 + 50 = 20050 + +**Better real example: (A * B) + C with ARITH** + +The ARITH unit can compute (D ± A) * B ± C. To get A * B + C, set D=0, use addition for the first term, and addition for the last term. You need a register that contains 0 (use s_zero). + +.. code-block:: text + + // (0 + r1) * r2 + r3 + ARITH PTP r1 r2 s_zero r3 + TEST -op(s_status AND #2) -uf // no WAIT macro for arith_dt -- poll by hand + JUMP HERE -if(Z) + REG_WR r4 op -op(s_arith_low) + + +5.3.1 ARITH Command Decoder: Understanding PTP, MTM, T, etc. + +The ARITH instruction uses a compact 2-3 letter code that encodes the entire formula. + +**Formula Pattern:** (D ± A) * B ± C + +Where: +- **A, B, C, D** are registers (rX, sY, or wZ) +- **D** is often s_zero (0) if not used + +**Decoding the mnemonic:** + +The mnemonic consists of up to 3 letters. Read them from LEFT to RIGHT: + +.. list-table:: ARITH Mnemonic Decoder + :header-rows: 1 + :widths: 15 25 50 + + * - Mnemonic + - Number of operands + - Formula + * - T + - 2 + - A * B + * - PT + - 3 + - (A + B) * C + * - MT + - 3 + - (A - B) * C + * - PTP + - 4 + - (A + B) * C + D + * - PTM + - 4 + - (A + B) * C - D + * - MTP + - 4 + - (A - B) * C + D + * - MTM + - 4 + - (A - B) * C - D + +**Memory trick to remember:** + +.. code-block:: text + + T = Times (multiply only) + P = Plus (addition in first part) + M = Minus (subtraction in first part) + TP = Times Plus (multiply then add) + TM = Times Minus (multiply then subtract) + + Read PT as "Plus then Times" → (A+B)*C + Read PTP as "Plus then Times then Plus" → (A+B)*C + D + +**Examples decoded:** + +.. code-block:: text + + ARITH T r1 r2 → r1 × r2 + ARITH PT r1 r2 r3 → (r1 + r2) × r3 + ARITH MT r1 r2 r3 → (r1 - r2) × r3 + ARITH PTP r1 r2 r3 r4 → (r1 + r2) × r3 + r4 + ARITH PTM r1 r2 r3 r4 → (r1 + r2) × r3 - r4 + ARITH MTP r1 r2 r3 r4 → (r1 - r2) × r3 + r4 + ARITH MTM r1 r2 r3 r4 → (r1 - r2) × r3 - r4 + +**Quick Reference Card for ARITH:** + +.. code-block:: text + + ┌─────────────────────────────────────────────────────────────────┐ + │ USE THIS CHEAT SHEET FOR ARITH │ + ├─────────────────────────────────────────────────────────────────┤ + │ │ + │ Need: A * B → ARITH T rA rB │ + │ Need: (A + B) * C → ARITH PT rA rB rC │ + │ Need: (A - B) * C → ARITH MT rA rB rC │ + │ Need: (A + B) * C + D → ARITH PTP rA rB rC rD │ + │ Need: (A + B) * C - D → ARITH PTM rA rB rC rD │ + │ Need: (A - B) * C + D → ARITH MTP rA rB rC rD │ + │ Need: (A - B) * C - D → ARITH MTM rA rB rC rD │ + │ │ + └─────────────────────────────────────────────────────────────────┘ + +**Complete Working Example:** + +.. code-block:: text + + // Calculate: (r1 + r2) * r3 + r4 + REG_WR r1 imm #10 + REG_WR r2 imm #20 + REG_WR r3 imm #3 + REG_WR r4 imm #5 + + ARITH PTP r1 r2 r3 r4 + TEST -op(s_status AND #2) -uf // no WAIT macro for arith_dt -- poll by hand + JUMP HERE -if(Z) + REG_WR r5 op -op(s_arith_low) // r5 = (10+20)*3+5 = 95 + +5.4. Division (DIV) +-------------------- + +The DIV block performs **unsigned** 32‑bit integer division in 32 cycles. + +**Syntax:** + +.. code-block:: text + + DIV numerator denominator + +- numerator: a register (rX, sY, or wZ) +- denominator: a register OR a 24‑bit immediate (#value) + +**Example: divide r1 by r2, get quotient and remainder** + +.. code-block:: text + + DIV r1 r2 // start division + WAIT div_dt // wait 32 cycles (macro) + REG_WR r3 op -op(s_div_q) // r3 = quotient + REG_WR r4 op -op(s_div_r) // r4 = remainder + +**Important:** Division is unsigned only. For signed division, convert to positive, divide, then adjust sign. + +5.5. Flow Control +----------------- + +**Unconditional jump:** + +.. code-block:: text + + JUMP LABEL // always jump + +**Conditional jump (most common for loops):** + +.. code-block:: text + + JUMP LABEL -if(NZ) // jump if last ALU result was not zero + +**Conditional jump with decrement (standard loop pattern):** + +.. code-block:: text + + REG_WR r0 imm #10 // r0 = 10 (loop counter) + LOOP: + // ... do something ... + JUMP LOOP -wr(r0 op) -op(r0 - #1) -uf // decrement r0, update flags, jump if not zero + +**Avoiding the branch penalty:** + +Because branches cause a 2‑cycle pipeline flush, use conditional execution for simple cases: + +.. code-block:: text + + // Instead of: + // JUMP SKIP -if(Z) + // REG_WR r1 imm #1 + // SKIP: + + // Do: + REG_WR r1 imm #1 -if(NZ) // only executed if Z flag is NOT set + +5.6. Memory Addressing Modes - Detailed Reference +------------------------------------------------- + +The tProcessor supports multiple addressing modes for different memory types. +Understanding these modes is crucial for writing efficient code. + +5.6.1. DMEM Addressing Modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Data Memory (DMEM) supports 4 addressing modes. The address is computed as +``rsA0 + rsA1`` in the RTL (``qcore_cpu.sv``), where each source can be an +11-bit literal or a 6-bit register specifier. The ``DF`` field controls the +immediate size when literals are used. + +.. list-table:: DMEM Addressing Modes + :header-rows: 1 + :widths: 20 20 20 40 + + * - Mode + - Syntax + - AI + - Example + * - Literal + - ``[&imm]`` + - 1 + - ``DMEM_WR [&10] imm #42`` + * - Register + - ``[rX]`` + - 0 + - ``DMEM_WR [r1] imm #42`` + * - Indexed Literal + - ``[rX + &imm]`` + - 1 + - ``DMEM_WR [r1+&4] imm #42`` + * - Indexed Register + - ``[rX + rY]`` + - 0 + - ``DMEM_WR [r2+r3] imm #42`` + +**When to use each mode:** + +- **Literal**: Fixed addresses (constants, lookup tables) +- **Register**: Dynamic addressing (arrays with variable index) +- **Indexed Literal**: Arrays with base pointer + small offset +- **Indexed Register**: 2D arrays or complex data structures + +**Example: Array traversal with indexed literal** + +.. code-block:: text + + REG_WR r0 imm #0 ; index + REG_WR r1 imm #10 ; base address + LOOP: + REG_WR r2 dmem [r1 + r0] ; read array[r0] from base r1 + ; ... process r2 ... + REG_WR r0 op -op(r0 + #1) -uf + JUMP LOOP -if(NZ) + +5.6.2. WMEM Addressing Modes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Wave Memory (WMEM) supports 2 addressing modes. The address is taken from +``rsA0`` only (``qcore_cpu.sv``: ``x1_wave_addr = x1_rs_a0_dt[WMEM_AW-1:0]``). + +.. list-table:: WMEM Addressing Modes + :header-rows: 1 + :widths: 20 20 20 40 + + * - Mode + - Syntax + - AI + - Example + * - Literal + - ``[&imm]`` + - 1 + - ``WMEM_WR [&5]`` + * - Register + - ``[rX]`` + - 0 + - ``WMEM_WR [r0]`` + +**Example: Sequentially loading waveforms** + +.. code-block:: text + + REG_WR r0 imm #0 + LOOP: + REG_WR r_wave wmem [r0] ; load waveform from WMEM[r0] + WPORT_WR p1 r_wave @s_out_time + REG_WR r0 op -op(r0 + #1) -uf + JUMP LOOP -if(NZ) + + +5.6.3. PMEM Addressing (Branch Instructions) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Program Memory (PMEM) addressing is used only for JUMP, CALL, and RET: + +.. list-table:: PMEM Addressing Modes + :header-rows: 1 + :widths: 20 20 20 40 + + * - Mode + - Syntax + - Encoding (AI bits) + - Example + * - Immediate (Label) + - ``LABEL`` + - 001 + - ``JUMP LOOP`` + * - Register (s_addr) + - ``s_addr`` + - 010 + - ``JUMP s_addr`` + * - Special constants + - ``HERE``, ``PREV``, ``NEXT``, ``SKIP`` + - (assembler) + - ``JUMP HERE -if(Z)`` + +5.7. Dual Task Instructions - Unified Reference +----------------------------------------------- + +One of the most powerful features of the tProcessor is the ability to execute +**two operations in a single 72-bit instruction word**. The 16-bit code field +encodes the secondary tasks in dedicated bits: + +- ``bit 3`` (``-wr`` / **SDT**): dual register write enable. +- ``bit 7`` (``-wp`` / **SPT**): dual port write enable. +- ``bit 9`` (``-ww`` / **SWT**): dual wave-memory write enable. + +The dual-task enable bits are only honoured on instruction categories that +support them (see matrix below); otherwise the bit is reserved. The RTL +enforces this via the ``cfg_dual_*_en`` gating terms in +``qcore_cpu.sv``. + +5.7.1. Types of Dual Tasks +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Dual Task Types + :header-rows: 1 + :widths: 15 15 15 55 + + * - Type + - Assembler flag + - Code bit + - Description + * - Second Data Task + - ``-wr(dest, source)`` + - ``code[3]`` (SDT) + - Writes a register in the same instruction + * - Second Wave Task + - ``-ww [&addr]`` + - ``code[9]`` (SWT) + - Also writes the r_wave contents into WMEM at ``&addr`` (used with WPORT_WR) + * - Second Port Task + - ``-wp(port, source)`` + - ``code[7]`` (SPT) + - Also drives a wave port with r_wave / WMEM data (used with REG_WR) + +5.7.2. Rules for Dual Tasks +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +1. **Only ONE immediate value** per instruction total (the ``DF`` field is shared). +2. **The source for SDT** can be an ALU operation (``-op``) or an immediate (``imm``). +3. **The destination for SDT** can be any register (r, s, or r_wave). +4. **SPT (``-wp``)** is allowed with REG_WR (writes wave data to a port); **SWT (``-ww``)** is allowed with WPORT_WR (writes r_wave into WMEM at the same time the wave is played). +5. The ``COND`` field applies to the whole instruction (the primary task AND any active dual tasks); the dual tasks don't get their own condition. + +5.7.3. Instruction Support Matrix +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Dual Task Support by Instruction + :header-rows: 1 + :widths: 22 15 15 15 35 + + * - Instruction + - SDT (-wr) + - SWT (-ww) + - SPT (-wp) + - Notes + * - REG_WR (SRC=op/imm/dmem/label) + - ✅ + - ❌ + - ❌ + - SDT only on ``-op`` source + * - REG_WR (SRC=wmem) + - ✅ + - ❌ + - ✅ (-wp sets SO=1) + - Loads r_wave from WMEM and writes to a port + * - DMEM_WR + - ✅ + - ❌ + - ❌ + - + * - WMEM_WR + - ✅ + - ✅ (primary, ``-ww`` is what WMEM_WR *is*) + - ✅ + - Can also output the waveform to a port at the same time + * - DPORT_WR + - ✅ + - ❌ + - ❌ + - + * - TRIG + - ✅ + - ❌ + - ❌ + - + * - WPORT_WR (SRC=wmem) + - ✅ + - ❌ + - ✅ (primary) + - + * - WPORT_WR (SRC=r_wave) + - ✅ + - ✅ + - ✅ (primary) + - All three dual tasks available + * - JUMP/CALL/RET + - ✅ + - ❌ + - ❌ + - SDT executes before branch resolves + * - TEST (CFG header=000) + - ❌ + - ❌ + - ❌ + - No destination + +5.7.4. Practical Examples +^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Example 1: Increment counter while writing data** + +.. code-block:: text + + ; Without SDT (2 instructions, 2 cycles) + DPORT_WR p0 reg r5 @1000 + REG_WR r5 op -op(r5 + #1) + + ; With SDT (1 instruction, 1 cycle) + DPORT_WR p0 reg r5 @1000 -wr(r5 op) -op(r5 + #1) + +**Example 2: Loop with decrement and branch** + +.. code-block:: text + + REG_WR r0 imm #10 + LOOP: + ; ... loop body ... + JUMP LOOP -wr(r0 op) -op(r0 - #1) -uf -if(NZ) + ; Decrement, update flags, and branch in one cycle + +**Example 3: Load waveform into r_wave and play it on a port** + +.. code-block:: text + + REG_WR r_wave wmem [&5] -wp(p0, wmem) @1000 + ; Load waveform from WMEM[r0] and increment r0 + + +5.8. Peripherals Control Instructions - Bit Field Reference +----------------------------------------------------------- + +Instructions with `HEADER = 010` (binary) control the **internal** peripheral +blocks (TIME, FLAG, ARITH, DIV). Instructions with `HEADER = 011` control +the **external** peripheral blocks (NET, COM, PA, PB). The peripheral select +is encoded in the 3-bit ``CTRL_ADDR`` field (see tables below). The 4-bit +``OPERATION`` (or 5-bit for external peripherals) selects the sub-function. + +.. note:: + The instruction width is **72 bits**. Tables show bit positions from MSB (bit 71) to LSB (bit 0). + +5.8.1. TIME Instruction (Header=010, CTRL_ADDR=000) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TIME instruction controls the absolute time counter and reference time. + +**Bit Encoding:** + +.. list-table:: TIME Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 8 5 5 5 5 8 10 5 5 5 5 5 5 5 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65:63 + - 62:60 + - 59:55 + - 54:45 + - 44:39 + - 38:31 + - 30:23 + - 22:15 + - 14:7 + - 6:0 + * - **Field** + - HEADER + - AI + - DF_H + - DF_L + - COND + - CTRL_ADDR + - OPERATION + - (rsA0) + - (rsA1) + - (rd0) + - (rd1) + - (imm) + - (imm) + - RD + * - **Value** + - 010 + - a + - d + - f + - cond + - 000 + - see table + - 0 / reg addr + - 0 / reg addr + - 0 / reg addr + - 0 / reg addr + - imm[31:24] + - imm[23:16] + - dest + +.. list-table:: TIME_OP Field Values (assembler: ``C_OP``) + :header-rows: 1 + :widths: 15 20 65 + + * - OPERATION bits + - Mnemonic + - Effect + * - ``_0001`` + - ``rst`` + - ``t_abs = 0``, ``t_ref = 0``, core reset (source data ignored) + * - ``_0010`` + - ``updt`` + - ``t_abs = t_abs + Source Data`` (immediate or register) + * - ``_0100`` + - ``set_ref`` + - ``t_ref = Source Data`` (t_abs unchanged) + * - ``_1000`` + - ``inc_ref`` + - ``t_ref = t_ref + Source Data`` + +**Field Descriptions:** + +.. list-table:: + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 111 for peripheral instructions + * - AI (Address Immediate) + - 68:66 + - 0=Source is register, 1=Source is immediate value + * - DF (Data Format) + - 65:64 + - Source data format (00=32-bit immediate, 01=24-bit, etc.) + * - OPCODE + - 63:61 + - Always 010 for TIME instruction + * - TIME_OP + - 60:58 + - Operation code (see table above) + * - Source Data + - 57:0 + - Time value (immediate) or register address (if AI=0) + +**Assembly Syntax:** + +.. code-block:: text + + TIME rst // Reset absolute time + TIME set_ref r2 // Set reference time to value in r2 + TIME updt #15750 // Add 15750 to absolute time + TIME inc_ref #15750 // Add 15750 to reference time + +5.8.2. FLAG Instruction (Header=111) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The FLAG instruction modifies the internal flag bit. + +**Bit Encoding:** + +.. list-table:: ARITH Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 10 10 10 10 10 10 10 + + * - Row Type + - 71:69 + - 68:66 + - 65:64 + - 63:53 + - 52:42 + - 41:31 + - 30:20 + - 19:0 + * - **Field** + - HEADER + - AI/DF + - OPCODE + - OPERAND_A + - OPERAND_B + - OPERAND_C + - OPERAND_D + - (unused) + * - **Value** + - 111 + - varies + - see table + - reg addr + - reg addr + - reg addr + - reg addr + - 0 + +.. list-table:: FLAG_OP Field Values + :header-rows: 1 + :widths: 15 20 60 + + * - FLAG_OP (bits 60:59) + - Mnemonic + - Effect + * - 00 + - ``set`` + - Internal flag = 1 + * - 01 + - ``clr`` + - Internal flag = 0 + * - 10 + - ``inv`` + - Internal flag = ~internal flag + * - 11 + - (reserved) + - Reserved for future use + +**Assembly Syntax:** + +.. code-block:: text + + FLAG set // Set internal flag + FLAG clr // Clear internal flag + FLAG inv // Invert internal flag + +5.8.3. ARITH Instruction (Header=111) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The ARITH instruction starts a multiply-accumulate operation on the DSP. + +**Bit Encoding:** + +.. list-table:: ARITH Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 10 10 10 10 10 10 20 + + * - Row Type + - 71:69 + - 68:66 + - 65:64 + - 63:61 + - 60:50 + - 49:39 + - 38:28 + - 27:0 + * - **Field** + - HEADER + - AI/DF + - OPCODE + - OPERAND_A + - OPERAND_B + - OPERAND_C + - OPERAND_D + - (unused) + * - **Value** + - 111 + - varies + - see table + - reg addr + - reg addr + - reg addr + - reg addr + - 0 + +.. list-table:: ARITH OPCODE Values (bits 63:61) + :header-rows: 1 + :widths: 15 15 20 45 + + * - OPCODE + - Mnemonic + - Operands + - Formula + * - 000 + - T + - 2 + - A * B + * - 001 + - PT + - 3 + - (A + B) * C + * - 010 + - MT + - 3 + - (A - B) * C + * - 011 + - PTP + - 4 + - (A + B) * C + D + * - 100 + - PTM + - 4 + - (A + B) * C - D + * - 101 + - MTP + - 4 + - (A - B) * C + D + * - 110 + - MTM + - 4 + - (A - B) * C - D + +**Register Addressing:** Each operand field (60:50, 49:39, etc.) contains a 11-bit register address: +- Bits 10:8 = Register bank (000=r, 001=s, 010=w) +- Bits 7:0 = Register index (0-31) + +**Assembly Syntax:** + +.. code-block:: text + + ARITH T r1 r2 // r1 * r2 + ARITH PT r1 r2 r3 // (r1 + r2) * r3 + ARITH PTP r1 r2 r3 r4 // (r1 + r2) * r3 + r4 + +5.8.4. DIV Instruction (Header=111) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The DIV instruction starts unsigned 32-bit integer division. + +**Bit Encoding:** + +.. list-table:: DIV Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 5 5 5 5 8 10 10 10 20 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:53 + - 52:42 + - 41:31 + - 30:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - OPCODE + - NUMERATOR + - DENOMINATOR + - (unused) + - (unused) + * - **Value** + - 111 + - a + - i + - d + - f + - 001 + - reg addr + - reg addr + - 0 + - 0 + +**Field Descriptions:** + +- **AI bit (68:66)**: 0 = Denominator is register, 1 = Denominator is immediate (24-bit) +- **Numerator (60:50)**: Always a register address +- **Denominator (49:39)**: Register address or 24-bit immediate value + +**Assembly Syntax:** + +.. code-block:: text + + DIV r1 r2 // r1 / r2 (both registers) + DIV r1 #100 // r1 / 100 (immediate denominator) + +5.8.5. NET Instruction (Header=111) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The NET instruction communicates with the QICK network interface. + +**Bit Encoding:** + +.. list-table:: NET Instruction (72-bit encoding) + :header-rows: 2 + :widths: 15 10 10 15 15 35 + + * - Bit(s) + - 71:69 + - 68:66 + - 65:64 + - 63:61 + - 60:0 + * - **Field** + - HEADER + - AI/DF + - OPCODE + - NET_CMD + - DATA + * - **Value** + - 111 + - varies + - 100 + - see table + - varies + +.. list-table:: NET_CMD Field Values (bits 60:56) + :header-rows: 1 + :widths: 15 20 45 + + * - NET_CMD + - Mnemonic + - Description + * - 00000 + - ``get_net`` + - Count nodes in network + * - 00001 + - ``set_net`` + - Configure network nodes + * - 00010 + - ``sync_net`` + - Synchronize all nodes + * - 00011 + - ``get_st`` + - Read data from network node + +5.8.6. PA/PB Instructions (Header=111) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +PA and PB control custom peripherals. + +**Bit Encoding:** + +.. list-table:: PA/PB Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 10 10 10 10 10 10 10 10 10 + + * - Bit(s) + - 71:69 + - 68:66 + - 65:64 + - 63:61 + - 60:56 + - 55:44 + - 43:32 + - 31:20 + - 19:8 + - 7:0 + * - **Field** + - HEADER + - AI/DF + - OPCODE + - PERIPH_OP + - DATA_A + - DATA_B + - DATA_C + - DATA_D + - (unused) + - (unused) + * - **Value** + - 111 + - varies + - 101/110 + - varies + - reg/imm + - reg/imm + - reg/imm + - reg/imm + - 0 + - 0 + +**OPCODE Values:** + +- 101 = PA (Custom Peripheral A) +- 110 = PB (Custom Peripheral B) + +**Data Fields:** Each can be a register address or immediate value, depending on DF bits. + +5.8.7. Summary: Peripheral OPCODE Map +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Complete Peripheral OPCODE Reference + :header-rows: 1 + :widths: 15 20 20 45 + + * - OPCODE (bits 63:61) + - Instruction + - Operands + - Description + * - 000 + - ARITH + - 2-4 + - Multiply-accumulate (2 cycles) + * - 001 + - DIV + - 2 + - Unsigned integer division (32 cycles) + * - 010 + - TIME + - 0-1 + - Time counter control + * - 011 + - FLAG + - 0 + - Internal flag control + * - 100 + - NET + - 0-1 + - Network communication + * - 101 + - PA + - 4 + - Custom peripheral A + * - 110 + - PB + - 4 + - Custom peripheral B + * - 111 + - (reserved) + - + - Reserved for future use + +5.8.8. Programming Pattern +^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**Standard pattern for peripheral control:** + +.. code-block:: text + + // 1. Configure data source (if reading result) + REG_WR s_cfg imm src_arith // route ARITH result to s_core_r1 + + // 2. Clear any old flags + CLEAR arith + + // 3. Start peripheral operation + ARITH PTP r1 r2 r3 r4 + + // 4. Wait for completion (no WAIT macro for arith_dt -- poll by hand) + TEST -op(s_status AND #2) -uf + JUMP HERE -if(Z) + + // 5. Read result + REG_WR r0 op -op(s_arith_low) + + // 6. Clear flag for next use + CLEAR arith + +Related Status Bits (s_status): + +.. list-table:: Peripheral Status Bits + :header-rows: 1 + :widths: 20 20 20 25 + + * - Peripheral + - Ready Bit (RDY) + - New Data Bit (DT_NEW) + - Clear Method + * - ARITH + - bit 0 + - bit 1 + - CLEAR arith or read + * - DIV + - bit 2 + - bit 3 + - CLEAR div or read + * - QPA (Custom A) + - bit 8 + - bit 9 + - CLEAR qpa + * - QPB (Custom B) + - bit 10 + - bit 11 + - CLEAR qpb + * - Input Ports + - (none) + - bits 31:16 + - CLEAR port or read + +5.9. Subroutines (CALL/RET) +--------------------------- + +The tProc supports nested calls up to 256 levels deep. + +.. code-block:: text + + CALL MY_SUBROUTINE + // ... after subroutine returns, execution continues here + + MY_SUBROUTINE: + // ... do something ... + RET + +**Important:** The stack only stores return addresses, not local variables. Use registers or DMEM for local storage. + +5.10. Waiting (WAIT macro) - Detailed Reference +----------------------------------------------- + +The ``WAIT`` instruction is an **assembler macro** that expands to a small polling loop. +It is not a single hardware instruction but a convenient way to wait for conditions. + +5.10.1. WAIT Macro Expansion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**WAIT time @N - Expansion:** + +.. code-block:: text + + ; Original: WAIT time @1000 + ; Expands to: + TEST -op(s_usr_time - #1000) ; Compare current time with N + JUMP HERE -if(S) ; Loop if current_time < N + +**Bit encoding of expanded instructions:** + +.. list-table:: WAIT time @N Expansion Encoding + :header-rows: 1 + :widths: 45 35 60 + + * - Instruction + - Opcode + - Description + * - ``TEST -op(s_usr_time - #N)`` + - TEST (Header=000) + - Performs subtraction, updates flags. N is 32-bit immediate. + * - ``JUMP HERE -if(S)`` + - JUMP (Header=001) + - Conditional jump to current address if Negative flag is set. + +5.10.2. WAIT div_dt - Expansion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + ; Original: WAIT div_dt + ; Expands to: + TEST -op(s_status AND #8) ; Test DIV_DT_NEW bit (bit 3) + JUMP HERE -if(Z) ; Loop while bit is 0 (Z flag set) + +**Bit encoding:** + +.. list-table:: WAIT div_dt Expansion Encoding + :header-rows: 1 + :widths: 35 30 50 + + * - Instruction + - Opcode + - Description + * - ``TEST -op(s_status AND #8)`` + - TEST (Header=000) + - AND operation with mask 0x8, updates Z flag + * - ``JUMP HERE -if(Z)`` + - JUMP (Header=001) + - Conditional jump to current address if Z flag is set (bit not set) + +.. _wait-arith-dt-manual: + +5.10.3. Waiting for ARITH -- No Macro, Write It By Hand +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Unlike ``WAIT time``/``WAIT div_dt``/``WAIT port_dt`` (real assembler +macros, confirmed against ``Instruction.WAIT()`` in +``tprocv2_assembler.py``), **``WAIT arith_dt`` is not a recognized +operand** -- the assembler raises ``RuntimeError: No Recognized +Operation`` on it. Write the equivalent TEST + JUMP loop directly instead: + +.. code-block:: text + + TEST -op(s_status AND #2) ; Test ARITH_DT_NEW bit (bit 1) + JUMP HERE -if(Z) ; Loop while bit is 0 + +5.10.4. WAIT port_dt - Expansion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + ; Original: WAIT port_dt + ; Expands to: + TEST -op(s_status AND #0xFFFF0000) ; Test any PORT_DT_NEW bit + JUMP HERE -if(Z) ; Loop if no new data on any port + +5.10.5. Custom WAIT Macro (user-defined) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can create your own wait loops for any condition: + +.. code-block:: text + + ; Wait for QPA_RDY (bit 8 of s_status) + WAIT_QPA_RDY: + TEST -op(s_status AND #0x100) ; Test bit 8 + JUMP WAIT_QPA_RDY -if(Z) ; Loop if not ready + + ; Wait for a specific input port (e.g., port 2, bit 18) + WAIT_PORT2: + TEST -op(s_status AND #1<<18) ; Test bit 18 + JUMP WAIT_PORT2 -if(Z) + +5.11. Clearing Flags (CLEAR) - Detailed Reference +------------------------------------------------- + +The ``CLEAR`` instruction is an **assembler macro** that writes a one-shot value +to the ``s_ctrl`` byte (upper 8 bits of s2) to clear ``_NEW`` flags in +``s_status``. The bit self-clears the cycle after it is written. + +5.11.1. CLEAR Macro Expansion +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +**CLEAR arith - Expansion:** + +.. code-block:: text + + ; Original: CLEAR arith + ; Expands to: + REG_WR s_ctrl imm clr_arith ; s_ctrl byte bit 0 = 1 + +**Bit encoding of expanded instruction:** + +.. list-table:: CLEAR arith Expansion Encoding + :header-rows: 1 + :widths: 20 20 60 + + * - Instruction + - Opcode + - Description + * - ``REG_WR s_ctrl imm clr_arith`` + - REG_WR (Header=100) + - Writes immediate value to s_ctrl, setting bit 0 to 1 (one-shot) + +5.11.2. CLEAR Options and Corresponding s_ctrl Bits +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: CLEAR Options + :header-rows: 1 + :widths: 20 20 15 45 + + * - CLEAR Option + - s_ctrl Bit (in byte) + - 32-bit Value + - Clears + * - ``CLEAR arith`` + - 0 + - 0x00010000 + - ``ARITH_DT_NEW`` (s_status[1]) + * - ``CLEAR div`` + - 1 + - 0x00020000 + - ``DIV_DT_NEW`` (s_status[3]) + * - ``CLEAR qnet`` + - 2 + - 0x00040000 + - ``QNET_DT_NEW`` (s_status[5]) + * - ``CLEAR qcom`` + - 3 + - 0x00080000 + - ``QCOM_DT_NEW`` (s_status[7]) + * - ``CLEAR qpa`` + - 4 + - 0x00100000 + - ``QPA_DT_NEW`` (s_status[9]) + * - ``CLEAR qpb`` + - 5 + - 0x00200000 + - ``QPB_DT_NEW`` (s_status[11]) + * - ``CLEAR port`` + - 6 + - 0x00400000 + - All ``PORT_DT_NEW`` bits (s_status[31:16]) + +5.11.3. CLEAR Macro Expansion Example +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: text + + ; Original: CLEAR div + ; Expands to: + REG_WR s_ctrl imm #0x00020000 ; Set bit 1 of s_ctrl byte (one-shot) + + ; Note: Writing to s_ctrl is self-clearing. + ; The bit returns to 0 after one clock cycle. + +5.11.4. Direct s_ctrl Usage (without CLEAR macro) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can write directly to ``s_ctrl`` to clear specific flags: + +.. code-block:: text + + ; Clear multiple flags at once + REG_WR s_ctrl imm #0x00700000 ; Clear arith + div + qpa (bits 16,17,20) + + ; Clear using predefined constants (if defined in assembler) + REG_WR s_ctrl imm clr_arith | clr_div + +5.12. Port Instructions - Bit Field Reference +--------------------------------------------- + +Instructions with `HEADER = 110` (binary) control output ports (DPORT, TRIG, WPORT) +and input ports (DPORT_RD). + +.. note:: + The detailed 72-bit tables in this section describe field semantics but + **must be verified against the authoritative sources**: + ``qick_lib/qick/tprocv2_assembler.py`` (``Instruction.PORT_WR``) and + ``firmware/rtl_lib/processors/qcore_cpu.sv``. The RTL decodes port + instructions as ``110_AI_DF__COND__CFG___ADDR____DATA__RD`` where CFG + contains SO/TO/UF/Wr/Rdi/alu_op, ADDR carries the port number or wave + address, and DATA carries the immediate or register source. + +5.12.1. DPORT_WR (Data Port Write) - Header=110 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The DPORT_WR instruction writes 32-bit data to a digital output port at a specified time. + +**Bit Encoding:** + +.. list-table:: Instruction Encoding (72-bit) + :header-rows: 2 + :widths: 10 5 5 5 5 5 5 5 5 5 5 5 5 5 5 20 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63 + - 62 + - 61 + - 60 + - 59 + - 58 + - 57 + - 56 + - 55:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - (un) + - (un) + - (un) + - (un) + - (un) + - (un) + - (un) + - (un) + - (un) + - DATA + * - **Value** + - 110 + - 1 + - 1 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - 0 + - varies + +**Field Descriptions:** + +.. list-table:: DPORT_WR Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 110 for port instructions + * - AI (Address Immediate) + - 68:66 + - 111 = Data is immediate, 000 = Data is from register + * - DF (Data Format) + - 65:64 + - Source data format (00=32-bit immediate, 01=24-bit, etc.) + * - PORT + - 63:61 + - Port number (0-7 for DPORT) + * - TIME_SRC + - 60:59 + - 00 = Time from s_out_time, 01 = Immediate time (@value) + * - DATA_SRC + - 58:55 + - Source of data (varies by AI/DF) + * - Imm_Data/Reg_Addr + - 54:0 + - Immediate data value or register address + +**Time Encoding:** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - TIME_SRC bits (60:59) + - Meaning + - Example + * - 00 + - Use s_out_time register + - ``DPORT_WR p0 reg r0 @s_out_time`` + * - 01 + - Use immediate time value + - ``DPORT_WR p0 imm 1 @1000`` + +**Assembly Syntax:** + +.. code-block:: text + + DPORT_WR p0 imm 1 @1000 ; Write 1 to port 0 at time 1000 + DPORT_WR p2 reg r5 @s_out_time ; Write r5 to port 2 using s_out_time + +5.12.2. TRIG (Trigger Port Write) - Header=110 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TRIG instruction writes a single bit (0 or 1) to a trigger output port. + +**Bit Encoding:** + +.. list-table:: TRIG Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 5 5 5 5 10 10 10 10 20 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - (unused) + - PORT + - SET_CLR + - TIME_SRC + - (unused) + * - **Value** + - 110 + - 1 + - 1 + - 0 + - 0 + - 0 + - port # + - 0 or 1 + - 0 or 1 + - 0 + +**Field Descriptions:** + +.. list-table:: TRIG Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 110 for port instructions + * - PORT + - 63:61 + - Port number (0-31 for TRIG ports) + * - SET_CLR + - 60 + - 0 = Clear (write 0), 1 = Set (write 1) + * - TIME_SRC + - 59:58 + - 00 = Time from s_out_time, 01 = Immediate time (@value) + * - Imm_Time + - 57:0 + - Immediate time value (if TIME_SRC=01) + +**Assembly Syntax:** + +.. code-block:: text + + TRIG p0 set @150 ; Set trigger port 0 high at time 150 + TRIG p1 clr @200 ; Set trigger port 1 low at time 200 + TRIG p2 set @s_out_time ; Use s_out_time register + +5.12.3. WPORT_WR (Wave Port Write) - Header=110 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The WPORT_WR instruction writes 168-bit waveform parameters to an analog wave output port. + +**Bit Encoding:** + +.. list-table:: WPORT_WR Instruction (72-bit encoding) + :header-rows: 2 + :widths: 8 7 4 4 4 4 4 6 4 4 7 10 10 24 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:55 + - 54:45 + - 44:39 + - 38:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - WW + - PORT + - WP + - TI + - SRC + - ADDR_SRC + - DATA_SRC + - (unused) + * - **Value** + - 110 + - 1 + - 1 + - 0 + - 0 + - 0 or 1 + - port # + - 0 or 1 + - 0 or 1 + - varies + - varies + - varies + - 0 + +**Field Descriptions:** + +.. list-table:: WPORT_WR Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 110 for port instructions + * - WW (Write Wave) + - 64 + - 1 = Write wave as second task (SWT), 0 = not a second task + * - PORT + - 63:61 + - Wave port number (0-15) + * - WP (Write Port) + - 60 + - 1 = This is a second port task (SPT), 0 = primary task + * - TI (Time Immediate) + - 59 + - 0 = Use s_out_time, 1 = Use immediate time @value + * - SRC (Source Select) + - 58:55 + - 0000 = Source is r_wave, 0001 = Source is WMEM + * - ADDR_SRC + - 54:45 + - WMEM address (immediate or register, depending on AI/DF) + * - DATA_SRC + - 44:39 + - Source register for r_wave (if SRC=0000 and not immediate) + * - Imm_Time + - 38:0 + - Immediate time value (if TI=1) + +**Source Encoding (SRC bits 58:55):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 65 + + * - SRC + - Source + - Description + * - 0000 + - r_wave + - Use the r_wave register + * - 0001 + - wmem + - Load from WMEM address (requires ADDR_SRC) + +**Assembly Syntax:** + +.. code-block:: text + + WPORT_WR p0 r_wave @1000 ; Write r_wave to port 0 at time 1000 + WPORT_WR p1 wmem [&5] @s_out_time ; Write WMEM[5] to port 1 using s_out_time + WPORT_WR p2 r_wave ; Write r_wave at time from s_out_time + +5.12.4. DPORT_RD (Data Port Read) - Header=110 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The DPORT_RD instruction reads 64-bit data from an input port (AXI-Stream interface) +to s_port_l (s8) and s_port_h (s9). + +**Bit Encoding:** + +.. list-table:: DPORT_RD Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 10 5 5 5 5 10 10 30 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - (unused) + - PORT + - (unused) + * - **Value** + - 110 + - 0 + - 0 + - 0 + - 0 + - 0 + - port # + - 0 + +**Field Descriptions:** + +.. list-table:: DPORT_RD Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 110 for port instructions + * - PORT + - 63:61 + - Input port number (0-3 typically, up to 16 configurable) + +**Result Registers:** + +- s8 (s_port_l): Lower 32 bits of input data +- s9 (s_port_h): Upper 32 bits of input data + +**Status Bits:** + +- s_status[31:16] (PORT_DT_NEW): Bit N indicates new data on port N +- Cleared by reading the port or by ``CLEAR port`` + +**Assembly Syntax:** + +.. code-block:: text + + DPORT_RD p0 ; Read port 0 to s_port_l/s_port_h + DPORT_RD p1 -wr(r15 op) -op(r1-#1) ; Read port 1 with dual task + +5.12.5. Summary: Port Instruction OPCODE Map +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Port Instruction Summary + :header-rows: 1 + :widths: 20 20 20 40 + + * - Instruction + - Header + - Sub-fields + - Description + * - DPORT_WR + - 110 + - PORT, TIME_SRC, DATA_SRC + - Write 32-bit data to digital port + * - TRIG + - 110 + - PORT, SET_CLR, TIME_SRC + - Write single bit to trigger port + * - WPORT_WR + - 110 + - PORT, TI, SRC, WW, WP + - Write 168-bit waveform to analog port + * - DPORT_RD + - 110 + - PORT + - Read 64-bit from input port + +5.12.6. Dual Task Options for Port Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Port instructions can include dual tasks: + +- **-wr(rd, src)** : Second Data Task - writes a register +- **-uf** : Update flags (used with -wr) +- **-wp(port)** : Second Port Task - writes a wave port (only for WPORT_WR?) + +**Example with dual task:** + +.. code-block:: text + + DPORT_WR p0 reg r5 @s_out_time -wr(r1 imm) #100 + ; Primary: Write r5 to port 0 + ; Secondary: Write 100 to r1 (same cycle) + + TRIG p0 set @s_out_time -wr(r0 op) -op(r0 + #1) + ; Primary: Set trigger + ; Secondary: Increment r0 + +**Bit encoding for dual task (SDT) in port instructions:** + +.. list-table:: Dual Task Fields in Port Instructions + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - WR (Write Register) + - 57 + - 1 = Second Data Task present + * - UF (Update Flag) + - 56 + - 1 = Update ALU flags + * - RDI (Register Destination Immediate) + - 55 + - 0 = ALU source, 1 = Immediate source + * - ALU_OP + - 54:50 + - ALU operation code (for -op()) + +5.13. Memory Instructions - Bit Field Reference +------------------------------------------------ + +Instructions with `HEADER = 101` (binary) write to Data Memory (DMEM) or Wave Parameter Memory (WMEM). + +5.13.1. DMEM_WR (Data Memory Write) - Header=101 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The DMEM_WR instruction writes 32-bit data to Data Memory at a specified address. + +**Bit Encoding:** + +.. list-table:: DMEM_WR Instruction (72-bit encoding) + :header-rows: 2 + :widths: 7 5 4 4 4 4 5 5 4 4 4 7 7 7 6 6 6 6 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:55 + - 54:45 + - 44:39 + - 38:31 + - 30:23 + - 22:15 + - 14:7 + - 6:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - COND + - ADDR_MODE + - WR + - UF + - RDI + - ADDR_SRC0 + - ADDR_SRC1 + - DATA_SRC + - (unused) + - (unused) + - (unused) + - (unused) + * - **Value** + - 101 + - see table + - see table + - see table + - see table + - 0-7 + - see table + - 0/1 + - 0/1 + - 0/1 + - varies + - varies + - varies + - 0 + - 0 + - 0 + - 0 + +**Field Descriptions:** + +.. list-table:: DMEM_WR Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 101 for memory write instructions + * - AI (Address Immediate) + - 68:66 + - Address mode selector (see table below) + * - DF (Data Format) + - 65:64 + - Source data format (00=32-bit immediate, 01=24-bit, 10=16-bit, 11=register only) + * - COND + - 63:61 + - Condition code for conditional execution (0-7) + * - ADDR_MODE + - 60:58 + - 000=literal, 001=register, 010=indexed literal, 011=indexed register + * - WR (Write Register) + - 57 + - 1 = Second Data Task present (-wr) + * - UF (Update Flag) + - 56 + - 1 = Update ALU flags + * - RDI (Register Destination Immediate) + - 55 + - 0 = ALU source for SDT, 1 = Immediate source for SDT + * - ADDR_SRC0 + - 54:45 + - First address component (register or immediate) + * - ADDR_SRC1 + - 44:39 + - Second address component (register, for indexed modes) + * - DATA_SRC + - 38:31 + - Data to write (register address or immediate value) + +**Address Modes (ADDR_MODE bits 60:58):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 20 45 + + * - ADDR_MODE + - Mode + - Syntax + - Description + * - 000 + - Literal + - ``[&imm]`` + - Address is immediate value + * - 001 + - Register + - ``[rX]`` + - Address is in register + * - 010 + - Indexed Literal + - ``[rX + &imm]`` + - Address = register + immediate offset + * - 011 + - Indexed Register + - ``[rX + rY]`` + - Address = register + register + +**AI Field Encoding (bits 68:66):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 65 + + * - AI + - Meaning + - Description + * - 000 + - ADDR_SRC0 is register + - First address component is a register address + * - 001 + - ADDR_SRC0 is immediate (11-bit) + - First address component is an immediate value + * - 010 + - ADDR_SRC1 is register + - Second address component is a register (for indexed modes) + * - 100 + - ADDR_SRC1 is immediate + - Second address component is immediate (rare) + +**Assembly Syntax:** + +.. code-block:: text + + DMEM_WR [&10] imm #42 ; Literal addressing + DMEM_WR [r1] op -op(r0) ; Register addressing + DMEM_WR [r1+&4] imm #5 ; Indexed literal + DMEM_WR [r2+r3] imm #1 ; Indexed register + DMEM_WR [&10] imm #42 -wr(r5 op) -op(r0+r1) ; With SDT + +5.13.2. WMEM_WR (Wave Memory Write) - Header=101 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The WMEM_WR instruction writes the 168-bit r_wave register to Wave Parameter Memory. + +**Bit Encoding:** + +.. list-table:: WMEM_WR Instruction (72-bit encoding) + :header-rows: 2 + :widths: 8 7 4 4 4 4 4 6 4 4 7 10 10 24 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:55 + - 54:45 + - 44:39 + - 38:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - WW + - (unused) + - WP + - TI + - ADDR_MODE + - ADDR_SRC + - DATA_SRC + - (unused) + * - **Value** + - 101 + - see table + - see table + - see table + - see table + - 0/1 + - 0 + - 0/1 + - 0/1 + - see table + - varies + - varies + - 0 + +**Field Descriptions:** + +.. list-table:: WMEM_WR Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 101 for memory write instructions + * - WW (Write Wave) + - 64 + - 1 = This is a second wave task (SWT), 0 = primary task + * - WP (Write Port) + - 59 + - 1 = Second port task present (-wp) + * - TI (Time Immediate) + - 58 + - For SPT: 0 = use s_out_time, 1 = use immediate time + * - ADDR_MODE + - 57:56 + - 00 = Literal, 01 = Register + * - ADDR_SRC + - 55:45 + - WMEM address (immediate or register address) + * - DATA_SRC + - 44:39 + - Source of data (always r_wave, field may be unused) + +**Address Modes for WMEM:** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 20 45 + + * - ADDR_MODE + - Mode + - Syntax + - Description + * - 00 + - Literal + - ``[&imm]`` + - Address is immediate (10-bit) + * - 01 + - Register + - ``[rX]`` + - Address from register (5-bit register address) + +**Assembly Syntax:** + +.. code-block:: text + + WMEM_WR [&5] ; Write r_wave to WMEM address 5 + WMEM_WR [r0] ; Write r_wave to address in r0 + WMEM_WR [&5] -wr(r1 imm) #100 ; With SDT + WMEM_WR [&5] -wp(p1) ; With SPT (second port task) + +5.14. Register Instructions - Bit Field Reference +-------------------------------------------------- + +Instructions with `HEADER = 100` (binary) write to registers (DREG, SREG, WREG, or r_wave). + +5.14.1. REG_WR (Register Write) - Header=100 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The REG_WR instruction writes data to a register from various sources (immediate, ALU, DMEM, WMEM). + +**Bit Encoding:** + +.. list-table:: REG_WR Instruction (72-bit encoding) + :header-rows: 2 + :widths: 7 5 4 4 4 4 5 5 5 5 5 5 7 7 7 10 10 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:55 + - 54:50 + - 49:39 + - 38:31 + - 30:23 + - 22:15 + - 14:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - DST_BANK + - DST_IDX + - SRC_TYPE + - WR + - UF + - RDI + - ALU_OP + - SRC_A + - SRC_B + - IMM_DATA + - (unused) + * - **Value** + - 100 + - see table + - see table + - see table + - see table + - see table + - reg index + - see table + - 0/1 + - 0/1 + - 0/1 + - see table + - varies + - varies + - varies + - 0 + +**Field Descriptions:** + +.. list-table:: REG_WR Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 100 for register write instructions + * - DST_BANK (Destination Bank) + - 68:67 + - 00 = DREG (r0-r31), 01 = SREG (s0-s15), 10 = WREG (w0-w5), 11 = r_wave (special) + * - DST_IDX (Destination Index) + - 66:61 + - Register index within the bank (0-31) + * - SRC_TYPE (Source Type) + - 60:58 + - 000 = ALU operation (-op), 001 = DMEM, 010 = WMEM, 011 = Immediate + * - WR (Write Register for SDT) + - 57 + - 1 = Second Data Task present + * - UF (Update Flag) + - 56 + - 1 = Update ALU flags + * - RDI (Register Destination Immediate) + - 55 + - For SDT: 0 = ALU source, 1 = Immediate source + * - ALU_OP + - 54:50 + - ALU operation code (for SRC_TYPE=000) + * - SRC_A + - 49:39 + - First source register address + * - SRC_B + - 38:31 + - Second source register address (or immediate value for some modes) + * - IMM_DATA + - 30:0 + - Immediate data value (when SRC_TYPE=011 or part of ALU operation) + +**Destination Bank Encoding (bits 68:67):** + +.. list-table:: + :header-rows: 1 + :widths: 15 15 20 50 + + * - DST_BANK + - Bank + - Registers + - Example + * - 00 + - DREG + - r0-r31 + - ``REG_WR r5 ...`` + * - 01 + - SREG + - s0-s15 + - ``REG_WR s_cfg ...`` + * - 10 + - WREG + - w0-w5 + - ``REG_WR w_freq ...`` + * - 11 + - r_wave + - (special) + - ``REG_WR r_wave ...`` + +**Source Type Encoding (bits 60:58):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 65 + + * - SRC_TYPE + - Source + - Description + * - 000 + - ALU (-op) + - Source is result of ALU operation + * - 001 + - DMEM + - Source is Data Memory (requires address) + * - 010 + - WMEM + - Source is Wave Memory (requires address, destination must be r_wave) + * - 011 + - Immediate + - Source is immediate value (#value) + +**ALU Operation Codes (bits 54:50) - when SRC_TYPE=000:** + +.. list-table:: + :header-rows: 1 + :widths: 15 15 70 + + * - ALU_OP + - Operation + - Example + * - 00000 + - ADD + - ``-op(r0 + r1)`` + * - 00001 + - SUB + - ``-op(r0 - r1)`` + * - 00010 + - AND + - ``-op(r0 AND r1)`` + * - 00011 + - ASR + - ``-op(r0 ASR #2)`` + * - 00100 + - ABS + - ``-op(ABS r0)`` + * - 00101 + - MSH + - ``-op(MSH r0)`` + * - 00110 + - LSH + - ``-op(LSH r0)`` + * - 00111 + - SWP + - ``-op(SWP r0)`` + * - 01000 + - NOT + - ``-op(NOT r0)`` + * - 01001 + - OR + - ``-op(r0 OR r1)`` + * - 01010 + - XOR + - ``-op(r0 XOR r1)`` + * - 01110 + - SL + - ``-op(r0 SL #2)`` + * - 01111 + - SR + - ``-op(r0 SR #2)`` + +**Assembly Syntax:** + +.. code-block:: text + + REG_WR r0 imm #100 ; Immediate source + REG_WR r1 op -op(r0 + r2) ; ALU source + REG_WR r2 dmem [&10] ; DMEM source + REG_WR r_wave wmem [&5] ; WMEM source (to r_wave) + REG_WR s_addr label LOOP ; Label address to s_addr + REG_WR r0 op -op(r0 + #1) -uf -wr(r1 imm) #100 ; With SDT + +5.15. Configuration Instructions - Bit Field Reference +------------------------------------------------------- + +Instructions with `HEADER = 000` (binary) perform configuration operations without writing data. + +5.15.1. NOP (No Operation) - Header=000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The NOP instruction does nothing and takes one cycle. + +**Bit Encoding:** + +.. list-table:: NOP Instruction (72-bit encoding) + :header-rows: 2 + :widths: 20 20 60 + + * - Bit(s) + - 71:69 + - 68:0 + * - **Field** + - HEADER + - (unused) + * - **Value** + - 000 + - 0 + +**Assembly Syntax:** + +.. code-block:: text + + NOP + +5.15.2. TEST (Test and Update Flags) - Header=000 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The TEST instruction performs an ALU operation and updates flags without writing a register. + +**Bit Encoding:** + +.. list-table:: TEST Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 7 5 5 5 5 5 7 5 5 10 10 10 20 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58:50 + - 49:39 + - 38:31 + - 30:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - (unused) + - COND + - UF + - RDI + - ALU_OP + - SRC_A + - SRC_B + - (unused) + * - **Value** + - 000 + - see table + - see table + - see table + - see table + - 0 + - 0-7 + - 1 + - 0/1 + - see table + - varies + - varies + - 0 + +**Field Descriptions:** + +.. list-table:: TEST Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 000 for configuration instructions + * - COND + - 63:61 + - Condition code (typically 000 for ALWAYS) + * - UF + - 60 + - Must be 1 to update flags + * - RDI + - 59 + - 0 = ALU operation, 1 = Immediate (rare for TEST) + * - ALU_OP + - 58:50 + - ALU operation code (same as REG_WR) + * - SRC_A + - 49:39 + - First source register address + * - SRC_B + - 38:31 + - Second source register address or immediate value + +**Assembly Syntax:** + +.. code-block:: text + + TEST -op(r0 - #1) -uf ; Test if r0-1 == 0 or negative + TEST -op(s_status AND #8) -uf ; Test DIV_DT_NEW bit + TEST -op(r1 - #32768) -uf ; Compare with threshold + + +5.16. Branch Instructions - Bit Field Reference +----------------------------------------------- + +Instructions with `HEADER = 001` (binary) control program flow by modifying the Program Counter (PC). + +.. note:: + The 72-bit word structure for port (5.12) and branch (5.16) instructions is + ``HEADER_AI_DF__COND__{BT/CFG}___ADDR____DATA__RD``. The authoritative + encoding lives in ``qick_lib/qick/tprocv2_assembler.py`` (``Instruction.BRANCH``, + ``Instruction.PORT_WR``) and ``firmware/rtl_lib/processors/qcore_cpu.sv``. + The tables below describe field semantics; exact bit positions should be + verified against those sources. + +5.16.1. JUMP (Conditional Branch) - Header=001 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The JUMP instruction changes the PC to a specified address, conditionally or unconditionally. + +**Bit Encoding:** + +.. list-table:: JUMP Instruction (72-bit encoding) + :header-rows: 2 + :widths: 7 5 4 4 4 4 6 4 4 4 4 4 4 7 10 10 + + * - Bit(s) + - 71:69 + - 68 + - 67:66 + - 65:63 + - 62:61 + - 60 + - 59 + - 58 + - 57 + - 56 + - 55 + - 54:50 + - 49:39 + - 38:31 + - 30:0 + * - **Field** + - HEADER + - AI + - DF + - COND + - BT + - UF + - WR + - RDI + - (rsvd) + - (rsvd) + - (rsvd) + - ALU_OP + - ADDR + - DATA + - RD + * - **Value** + - 001 + - see table + - see table + - 0-7 + - see table + - 0/1 + - 0/1 + - 0/1 + - 0 + - 0 + - 0 + - see table + - varies + - varies + - 0 + +**Field Descriptions:** + +.. list-table:: JUMP Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 001 for branch instructions + * - AI (Address Immediate) + - 68 + - 1 = Address is immediate 11-bit literal, 0 = Address is from ``s_addr`` (s15) register + * - DF (Data Format) + - 67:66 + - Data format for SDT (if present); same encoding as other instructions + * - COND + - 65:63 + - Condition code for conditional execution (0-7, see Condition Codes table) + * - BT (Branch Type) + - 62:61 + - ``00`` = JUMP, ``10`` = CALL, ``11`` = RET. ``01`` is reserved. + * - UF (Update Flag) + - 60 + - 1 = Update ALU flags from the SDT ALU operation + * - WR (Write Register / SDT) + - 59 + - 1 = Second Data Task present (``-wr(...)``) + * - RDI + - 58 + - For SDT: 0 = ALU source (``-op``), 1 = Immediate source (``imm``) + * - ALU_OP + - 54:50 + - ALU operation code for SDT (if WR=1); same encoding as ``-op`` in REG_WR + * - ADDR + - 49:39 (11-bit) or 54:49 (6-bit) + ... + - Jump target: 11-bit immediate address (AI=1) or register address (AI=0, ``s_addr``) + * - DATA + - 38:0 + - Source data for SDT (if WR=1): immediate or register address + +**Branch Type (BT) Encoding (bits 62:61):** + +.. list-table:: Branch Type (BT) Encoding + :header-rows: 1 + :widths: 15 20 65 + + * - BT + - Instruction + - Description + * - ``00`` + - JUMP + - Unconditional or conditional branch + * - ``10`` + - CALL + - Branch with return address pushed to stack (8-deep hardware stack) + * - ``11`` + - RET + - Return from subroutine (pop address from stack) + * - ``01`` + - (RFU) + - Reserved + +**Address Modes (AI bit 68):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 65 + + * - AI + - Mode + - Description + * - 1 + - Immediate + - Address is 11-bit immediate value encoded in the ADDR field + * - 0 + - Register + - Address is read from ``s_addr`` (s15) register + +**Special Address Constants (assembler-only):** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Constant + - Meaning + - Effect + * - ``HERE`` + - Current address + - `PC = PC` (infinite loop) + * - ``PREV`` + - Previous instruction + - `PC = PC - 1` + * - ``NEXT`` + - Next instruction + - `PC = PC + 1` + * - ``SKIP`` + - Skip one instruction + - `PC = PC + 2` + +**Condition Codes (COND bits 65:63):** + +.. list-table:: + :header-rows: 1 + :widths: 15 20 15 50 + + * - COND + - Condition + - Code + - Description + * - 000 + - ALWAYS + - (always) + - Unconditional jump + * - 001 + - Z + - Zero + - Jump if Zero flag = 1 + * - 010 + - S + - Sign (Negative) + - Jump if Sign flag = 1 + * - 011 + - NZ + - Non-Zero + - Jump if Zero flag = 0 + * - 100 + - NS + - Not Sign + - Jump if Sign flag = 0 + * - 101 + - F + - Flag + - Jump if selected flag is high + * - 110 + - NF + - Not Flag + - Jump if selected flag is low + * - 111 + - RFU + - - + - Reserved + +**Assembly Syntax:** + +.. code-block:: text + + JUMP LABEL ; Unconditional jump to LABEL + JUMP LABEL -if(NZ) ; Conditional jump if not zero + JUMP HERE -if(Z) ; Infinite loop if zero flag set + JUMP PREV -if(S) ; Jump to previous instruction + JUMP s_addr ; Jump to address in s_addr + JUMP LABEL -wr(r0 op) -op(r0 - #1) -uf ; Jump with SDT (decrement counter) + +5.16.2. CALL (Subroutine Call) - Header=001 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The CALL instruction saves the current PC to the stack and jumps to the specified address. + +**Bit Encoding:** + +.. list-table:: CALL Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 8 4 4 4 4 4 8 4 4 4 42 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58 + - 57:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - BT + - COND + - UF + - WR + - RDI + - ADDR_SRC + * - **Value** + - 001 + - see table + - see table + - see table + - see table + - 1 + - 0-7 + - 0/1 + - 0/1 + - 0/1 + - varies + +**Field Descriptions:** + +.. list-table:: CALL Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 001 for branch instructions + * - BT (Branch Type) + - 64 + - 1 = CALL (0 = JUMP) + * - COND + - 63:61 + - Condition code (0-7) - CALL can be conditional! + * - ADDR_SRC + - 57:0 + - Target address (immediate or register) + * - UF, WR, RDI + - 60,59,58 + - Same as JUMP - for SDT support + +**Stack Operation:** + +- Pushes current PC (return address) to hardware stack +- Stack depth is 256 levels +- Stack is **only** for return addresses, not local variables + +**Assembly Syntax:** + +.. code-block:: text + + CALL SUBROUTINE ; Call subroutine + CALL SUBROUTINE -if(NZ) ; Conditional call + CALL s_addr ; Call address in s_addr + CALL SUBROUTINE -wr(r0 imm) #1 ; Call with SDT + +5.16.3. RET (Return from Subroutine) - Header=001 +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The RET instruction pops the return address from the stack and jumps back to the caller. + +**Bit Encoding:** + +.. list-table:: RET Instruction (72-bit encoding) + :header-rows: 2 + :widths: 10 8 4 4 4 4 4 8 4 4 4 42 + + * - Bit(s) + - 71:69 + - 68 + - 67 + - 66 + - 65 + - 64 + - 63:61 + - 60 + - 59 + - 58 + - 57:0 + * - **Field** + - HEADER + - AI_H + - AI_L + - DF_H + - DF_L + - BT + - COND + - UF + - WR + - RDI + - (unused) + * - **Value** + - 001 + - 0 + - 0 + - 0 + - 0 + - 2 + - 7 + - 0/1 + - 0/1 + - 0/1 + - 0 + +**Field Descriptions:** + +.. list-table:: RET Fields + :header-rows: 1 + :widths: 15 15 70 + + * - Field + - Bits + - Description + * - HEADER + - 71:69 + - Always 001 for branch instructions + * - BT (Branch Type) + - 64 + - 10 or 2? (RET is a special encoding) + * - COND + - 63:61 + - Typically 111 (ALWAYS) - RET is normally unconditional + * - UF, WR, RDI + - 60,59,58 + - For SDT support (rarely used with RET) + +**Stack Operation:** + +- Pops return address from hardware stack +- PC = popped address +- Stack depth decreases by 1 + +**Assembly Syntax:** + +.. code-block:: text + + RET ; Return to caller + RET -wr(r0 op) -op(r0 + #1) ; Return with SDT (rare) + +5.16.4. Summary: Branch Instructions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: Branch Instruction Summary + :header-rows: 1 + :widths: 20 15 20 45 + + * - Instruction + - Header + - BT Bit + - Description + * - JUMP + - 001 + - 0 + - Conditional/unconditional branch + * - CALL + - 001 + - 1 + - Branch with stack push (return address) + * - RET + - 001 + - 10 (special) + - Return from CALL (stack pop) + +**Pipeline Behavior:** + +.. list-table:: + :header-rows: 1 + :widths: 20 20 60 + + * - Instruction + - Latency + - Pipeline Effect + * - JUMP (taken) + - 2 cycles + - Pipeline flushed, 2 instruction slots wasted + * - JUMP (not taken) + - 1 cycle + - No flush, continues normally + * - CALL + - 2 cycles + - Pipeline flush + stack push + * - RET + - 2 cycles + - Pipeline flush + stack pop + +**Conditional Branch Optimization:** + +Branch instructions cause a 2-cycle penalty when taken. For simple conditional execution, use conditional instruction execution instead: + +.. code-block:: text + + ; Instead of: + ; JUMP SKIP -if(Z) + ; REG_WR r0 imm #1 + ; SKIP: + + ; Do this (no branch penalty): + REG_WR r0 imm #1 -if(NZ) + +**Stack Depth:** Maximum 256 nested CALLs. Exceeding this will overwrite the stack (undefined behavior). + +5.16.5. Branch Instructions with SDT (Second Data Task) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +All branch instructions can include a Second Data Task (-wr) that executes **before** the branch is taken. + +.. code-block:: text + + JUMP LOOP -wr(r0 op) -op(r0 - #1) -uf -if(NZ) + ; 1. Decrement r0 and update flags + ; 2. If NZ flag set, jump to LOOP + ; If NZ flag not set, fall through + +**Use case:** Loop counter management in a single instruction. + +.. code-block:: text + + REG_WR r0 imm #10 + LOOP: + ; ... loop body ... + JUMP LOOP -wr(r0 op) -op(r0 - #1) -uf -if(NZ) + ; Decrement and branch in one instruction + + +------------------------------------------------------------------------------- + +6. Time Management +================== + +6.1. Time Variables Recap +------------------------- + +- ``t_abs``: 48‑bit counter, runs at DAC frequency (t_clk). Counts absolute time since last reset. +- ``t_ref``: 48‑bit reference. Defines where user time = 0. +- ``t_usr = t_abs - t_ref``: user time, available in ``s_usr_time``. +- ``t_out_user``: 32‑bit signed value you specify in port writes. +- ``t_abs_out = t_ref + t_out_user``: absolute time when output will occur. + +**Why signed t_out_user?** +You can schedule outputs with a **negative** offset, meaning "output at time X relative +to reference, even if reference is in the past". This is useful for synchronizing with +other boards or for pre‑calculated sequences where the reference moves. + +6.2. TIME Instructions +---------------------- + +.. code-block:: text + + TIME rst // t_abs = 0, t_ref = 0, core resets + TIME set_ref rX // t_ref = rX (t_abs unchanged) + TIME inc_ref #N // t_ref = t_ref + N + TIME updt #N // t_abs = t_abs + N (rarely used) + +**Typical initialization sequence:** + +.. code-block:: text + + TIME rst // start from zero + // ... wait for external trigger ... + TIME set_ref r0 // once triggered, set t_ref to current t_abs (so t_usr becomes 0) + +6.3. Scheduling Outputs +------------------------ + +**Fixed time (immediate):** + +.. code-block:: text + + WPORT_WR p0 r_wave @1000 // output at t_usr = 1000 + +**Relative to previous output:** + +.. code-block:: text + + REG_WR s_out_time imm #1000 + WPORT_WR p0 r_wave @s_out_time + REG_WR s_out_time op -op(s_out_time + #500) + WPORT_WR p0 r_wave @s_out_time // 500 cycles after first + +**Variable time from register:** + +.. code-block:: text + + REG_WR r0 imm #2000 + WPORT_WR p0 r_wave @r0 // @r0 is allowed? Check syntax. + +**Note:** The assembler accepts ``@rX`` (time from register) but the manual shows only immediate or s_out_time. Verify. + +6.4. Waiting for a Specific Time +-------------------------------- + +.. code-block:: text + + WAIT time @5000 // pause execution until t_usr >= 5000 + +6.5. Reset and Initialization Sequence +--------------------------------------- + +A proper initialization sequence ensures predictable behavior. + +**Complete Initialization Checklist:** + +.. code-block:: text + + ; 1. Reset everything + TIME rst + + ; 2. Configure data sources (if using peripherals) + REG_WR s_cfg imm src_arith + + ; 3. Clear stale flags + CLEAR arith + CLEAR div + CLEAR port + + ; 4. Initialize time registers + REG_WR s_out_time imm #1000 + + ; 5. Load initial waveform parameters + REG_WR w_freq imm #0x1000000 + REG_WR w_phase imm #0 + REG_WR w_gain imm #32768 + REG_WR w_length imm #1024 + + ; 6. Save to WMEM (optional) + WMEM_WR [&0] + + ; 7. Initialize loop counters + REG_WR r0 imm #100 + + ; 8. Start main program + JUMP MAIN + +**What TIME rst does:** + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Component + - Effect + * - t_abs, t_ref + - Set to 0 + * - Program Counter + - Set to 0 + * - All registers (r, s, w) + - Cleared to 0 + * - FIFOs + - Flushed + * - AXI registers + - NOT cleared + * - LFSR (s_rand) + - NOT cleared + +**Python Initialization:** + +.. code-block:: python + + from qick import QickSoc + + soc = QickSoc() + tproc = soc.tproc + + # Reset time and core + tproc.time_reset() + + # Load program binary (list of 32-bit words) + tproc.load_bin_program(prog_bin, load_mem=True) + + # Start execution + tproc.start() + + # Stop execution + tproc.stop() + + # Read DMEM + data = tproc.read_dmem(addr=0x10, length=100) + + # Write DMEM (buffers) + tproc.load_dmem(buff_in=data, addr=0x20) + + # Config registers are plain Python properties, not raw single_read/ + # single_write (those exist but the driver marks them "Do not use! + # Use the DMA instead" -- see set_lfsr_cfg() for this specific example) + tproc.set_lfsr_cfg(mode=2) # core_cfg LFSR field = 2 (advance-on-read) + val = tproc.core_cfg # read back the whole core_cfg register + + # External flag + tproc.set_axi_flg() + tproc.clr_axi_flg() + + +------------------------------------------------------------------------------- + +7. Input/Output Programming +=========================== + +7.1. Output Ports +----------------- + +**Data ports (DPORT)**: 32‑bit digital outputs + +.. code-block:: text + + DPORT_WR p0 imm 0x12345678 @1000 + DPORT_WR p2 reg r5 @2000 + +**Trigger ports (TRIG)**: 1‑bit outputs (set high or low) + +.. code-block:: text + + TRIG p0 set @150 // output 1 + TRIG p0 clr @200 // output 0 + +**Wave ports (WPORT)**: 168‑bit waveform parameters + +.. code-block:: text + + WPORT_WR p1 r_wave @1000 // from r_wave + WPORT_WR p1 wmem [&5] @2000 // from WMEM address 5 + +7.2. Input Ports (Feedback) +--------------------------- + +Input ports receive data from ADCs or other sources via AXI‑Stream. + +**Reading a single port (64 bits total):** + +.. code-block:: text + + WAIT port_dt // wait until any input port has new data + DPORT_RD p0 // read port 0 -> s_port_l (lower 32) and s_port_h (upper 32) + REG_WR r0 op -op(s_port_l) // use lower part + +**Checking which port received data (advanced):** + +The ``s_status`` register has bits 31:16 indicating which port has fresh data. You can test individual bits. + +.. code-block:: text + + // Wait specifically for port 2 (bit 18) + WAIT_PORT2: + TEST -op(s_status AND #(1<<18)) + JUMP WAIT_PORT2 -if(Z) + DPORT_RD p2 + +7.3. External Flag (Python to tProc) +------------------------------------ + +Python can set or clear an external flag that the tProc can test with ``-if(F)`` or ``-if(NF)``. + +**From Python:** + +.. code-block:: python + + soc.tproc.set_external_flag() # sets bit 13 of tproc_ctrl + soc.tproc.clear_external_flag() # sets bit 14 + +**In assembly:** + +.. code-block:: text + + WAIT_FOR_FLAG: + REG_WR r0 imm #0 -if(NF) // do nothing if flag not set + JUMP WAIT_FOR_FLAG -if(NF) // loop if flag not set + // flag is set, proceed + +------------------------------------------------------------------------------- + +.. _tproc-peripherals: + +8. Peripherals Deep Dive +======================== + +8.1. ARITH (Multiply‑Accumulate) +-------------------------------- + +The ARITH unit is the most powerful peripheral. It can replace multiple ALU instructions. + +**Configuration before using ARITH:** + +.. code-block:: text + + // 1. Set data source for s_core_r1 to ARITH + REG_WR s_cfg imm src_arith + + // 2. Clear any old new‑data flag + REG_WR s_ctrl imm clr_arith + +**Start an ARITH operation:** + +.. code-block:: text + + ARITH T r1 r2 // r1 * r2 (4 cycles) + +**Wait for completion** (no ``WAIT`` macro for this -- poll by hand, see +:ref:`5.10.3 `): + +.. code-block:: text + + TEST -op(s_status AND #2) -uf + JUMP HERE -if(Z) + +**Read the result (64 bits):** + +.. code-block:: text + + ; Lower 32 bits via s_arith_low (s3) + REG_WR r_low op -op(s_arith_low) + + ; Upper 32 bits via AXI read (READ_SEL = 4 -> TPROC_R_DT = {low, high}) + ; or by reading s_arith_low twice (the register bank presents the full + ; 64-bit result as two consecutive 32-bit reads when selected by DT_SRC). + +**Note:** The ARITH unit produces a 64‑bit result. ``s_arith_low`` (s3) always +holds the lower 32 bits. The full 64-bit result is available through the AXI +``TPROC_R_DT`` register when ``READ_SEL = 4`` (``src_arith``). + +8.2. DIV (Integer Division) +--------------------------- + +Division is slow (32 cycles) but non‑blocking: the CPU continues execution while DIV works in the background. + +**Best practice:** Start a division early, do other work, then check for completion. + +.. code-block:: text + + // Start division early + DIV r1 r2 + + // Do other useful work while division runs + REG_WR r3 op -op(r3 + #100) + REG_WR r4 op -op(r4 - #50) + + // Wait for division to complete + WAIT div_dt + REG_WR r5 op -op(s_div_q) // quotient + REG_WR r6 op -op(s_div_r) // remainder + +8.3. LFSR (Random Number Generator) +------------------------------------ + +Configure via ``core_cfg`` AXI register. + +**Modes:** + +- 0: Stop (do not advance) +- 1: Free running (advance every c_clk cycle) +- 2: Advance when read +- 3: Advance when written (write any value to s_rand) + +**Example: advance‑on‑read mode** + +.. code-block:: text + + // Configure from Python (soc.tproc.set_lfsr_cfg(mode=2) -- see qick.drivers.tproc) + + // In assembly: each read gives a new random number + REG_WR r0 op -op(s_rand) // first random + REG_WR r1 op -op(s_rand) // second random (different) + +------------------------------------------------------------------------------- + +.. _tproc-examples: + +9. Task‑Based Examples (Copy‑Paste Ready) +========================================== + +9.1. Generate a Single Pulse +---------------------------- + +.. code-block:: text + + .ADDR 0x00 + // Configure waveform + REG_WR w_freq imm #0x1000000 // 1 MHz (example) + REG_WR w_phase imm #0 + REG_WR w_gain imm #32768 // half amplitude + REG_WR w_length imm #1000 // 1000 samples + REG_WR w_conf imm #0 + + // Output at time 1000 + WPORT_WR p1 r_wave @1000 + .END + +9.2. Generate a Pulse Train +--------------------------- + +.. code-block:: text + + .ADDR 0x00 + REG_WR r0 imm #10 // number of pulses + REG_WR s_out_time imm #1000 // start time + + LOOP: + WPORT_WR p1 r_wave @s_out_time + REG_WR s_out_time op -op(s_out_time + #500) // 500 cycle spacing + REG_WR r0 op -op(r0 - #1) -uf + JUMP LOOP -if(NZ) + .END + +9.3. Conditional Pulse Based on ADC Value +------------------------------------------ + +.. code-block:: text + + .ADDR 0x00 + // Initialization + REG_WR s_out_time imm #1000 + REG_WR r0 imm #100 + + // Configure waveform A (long pulse) + REG_WR w_freq imm #0x1000000 + REG_WR w_gain imm #32768 + REG_WR w_length imm #2000 + REG_WR w_conf imm #0 + // Save a copy to WMEM + WMEM_WR [&0] + + // Configure waveform B (short pulse) + REG_WR w_length imm #500 + WMEM_WR [&1] + + // Main loop + REG_WR r0 imm #100 // number of experiments + + EXPERIMENT: + WAIT port_dt // wait for ADC data + DPORT_RD p0 // read port 0 + REG_WR r1 op -op(s_port_l) // r1 = ADC value + REG_WR r1 op -op(r1 - #32768) -uf // compare with threshold + + // Select waveform based on comparison + ; Note: REG_WR with wmem source cannot be conditional. + ; Use an unconditional load after setting r_wave via an intermediate register. + REG_WR r2 op -op(r1) -if(NS) // r2 = r1 (copy) if ADC >= 32768 + REG_WR r_wave wmem [&0] // long pulse (default) + REG_WR r_wave wmem [&1] -if(S) // short pulse if ADC < 32768 + ; Alternatively, use a TEST + JUMP pattern instead of conditional REG_WR. + + WPORT_WR p1 r_wave @s_out_time + REG_WR s_out_time op -op(s_out_time + #2000) // wait for next experiment + + REG_WR r0 op -op(r0 - #1) -uf + JUMP EXPERIMENT -if(NZ) + .END + +9.4. Frequency Sweep (Chirp) +---------------------------- + +.. code-block:: text + + .ADDR 0x00 + REG_WR r0 imm #100 // number of steps + REG_WR r1 imm #0x1000000 // start frequency + REG_WR s_out_time imm #1000 + +LOOP: + REG_WR w_freq op -op(r1) + WPORT_WR p1 r_wave @s_out_time + REG_WR r1 op -op(r1 + #0x10000) // increment frequency + REG_WR s_out_time op -op(s_out_time + #500) + REG_WR r0 op -op(r0 - #1) -uf + JUMP LOOP -if(NZ) + .END + +9.5. Wait for External Trigger (from Python flag) +-------------------------------------------------- + +.. code-block:: text + + .ADDR 0x00 + // Wait for Python to set external flag + WAIT_TRIG: + JUMP WAIT_TRIG -if(NF) // loop while flag is 0 + // Flag is set, continue + WPORT_WR p1 r_wave @1000 + .END + +From Python: + +.. code-block:: python + + soc.tproc.set_external_flag() # triggers the tProc + +9.6. Read and Accumulate ADC Data +--------------------------------- + +.. code-block:: text + + .ADDR 0x00 + REG_WR r0 imm #0 // accumulator + REG_WR r1 imm #100 // number of samples + +LOOP: + WAIT port_dt + DPORT_RD p0 + REG_WR r0 op -op(r0 + s_port_l) // accumulate + REG_WR r1 op -op(r1 - #1) -uf + JUMP LOOP -if(NZ) + + // Store result to DMEM + DMEM_WR [&0] imm r0 // result at DMEM address 0 + .END + +------------------------------------------------------------------------------- + +10. System Configuration (Hardware Parameters) +============================================== + +The tProcessor can be configured at synthesis time (in the FPGA bitstream) to match your needs. The following parameters trade off resource usage against capabilities. + +10.1. Memory Sizes +------------------ + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Width (bits) + - Minimum + - Maximum + - Default (QICK) + - When to increase + * - PMEM_AW (Program Memory address bits) + - 2^AW words + - 8 (256 words) + - 16 (65536) + - 8 (256 words) + - Complex programs with many instructions + * - DMEM_AW (Data Memory address bits) + - 2^AW words + - 8 (256) + - 16 (65536) + - 8 (256) + - Large lookup tables or data buffers + * - WMEM_AW (Wave Memory address bits) + - 2^AW words + - 8 (256) + - 11 (2048) + - 8 (256) + - Many pre‑computed waveforms + * - REG_AW (Number of data registers) + - 2^AW registers + - 4 (16 registers) + - 5 (32 registers) + - 4 (16 registers) + - Usually fine at 32 + +10.2. FIFO Depths +----------------- + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Default + - Maximum + - Resource cost + * - FIFO_DEPTH (all three FIFOs) + - 8 (tprocv2 core) / 9 (packaged IP top) + - 512 + - Increases BRAM usage + +**Choosing FIFO depth:** Set to the maximum number of pending port writes you may have. If your program writes 20 ports between waiting periods, set depth ≥ 20. + +10.3. Port Counts +----------------- + +.. list-table:: + :header-rows: 1 + + * - Parameter + - Default (QICK) + - Maximum + * - IN_PORT_QTY (input ports) + - 1 + - 16 + * - OUT_TRIG_QTY (trigger ports) + - 2 + - 8 + * - OUT_DPORT_QTY (data ports) + - 1 + - 4 + * - OUT_DPORT_DW (data port width, bits) + - 4 + - 32 + * - OUT_WPORT_QTY (wave ports) + - 1 + - 16 + +**Note:** Increasing port counts increases FPGA resource usage and may reduce maximum clock frequency. + +------------------------------------------------------------------------------- + +11. Complete Instruction Reference (Alphabetical) +================================================== + +11.1. ARITH +----------- + +**Purpose:** Start a multiply‑accumulate operation on the DSP. + +**Syntax:** ``ARITH op A B [C] [D]`` + +**Operation codes (selected by ``alu_op_i[3:0]``; the DSP macro's A/D inputs share a pre-adder, so the form is ``(A op D) * B [+|- C]``):** + +- ``T`` : ``A * B`` +- ``TP`` : ``A * B + C`` +- ``TM`` : ``A * B - C`` +- ``PT`` : ``(D + A) * B`` +- ``PTP``: ``(D + A) * B + C`` +- ``PTM``: ``(D + A) * B - C`` +- ``MT`` : ``(D - A) * B`` +- ``MTP``: ``(D - A) * B + C`` +- ``MTM``: ``(D - A) * B - C`` + +**Operand widths (DSP48E2):** A and D = 27 bits, B = 18 bits, C = 32 bits, result = 46 bits (sign-extended to 64 bits in ``sreg_arith``). + +**Latency:** 4 c_clk cycles (the DSP macro is held busy for 4 cycles; ``arith_rdy`` goes low while working and returns high when done). + +**Flags affected:** None (but sets ``ARITH_DT_NEW`` in ``s_status`` after the result is captured). + +**Example:** ``ARITH T r1 r2`` + +11.2. CALL +---------- + +**Purpose:** Call a subroutine (save PC to stack). + +**Syntax:** ``CALL address`` + +**Address can be:** label, immediate (&addr), or s_addr. + +**Latency:** 2 cycles (branch penalty) + +**Max nesting:** 8 (hardware stack depth, fixed in ``tpv2_stack``) + +**Example:** ``CALL MY_SUB`` + +11.3. CLEAR (macro) +------------------- + +**Purpose:** Clear ``_NEW`` flag in s_status. + +**Syntax:** ``CLEAR option`` + +**Options:** ``arith``, ``div``, ``port``, ``qnet`` + +**Expands to:** ``REG_WR s_ctrl imm value`` + +**Example:** ``CLEAR arith`` + +11.4. DIV +--------- + +**Purpose:** Start unsigned 32‑bit integer division. + +**Syntax:** ``DIV numerator denominator`` + +- numerator: register +- denominator: register or 24‑bit immediate + +**Latency:** 32 cycles (non‑blocking) + +**Results:** s_div_q (quotient), s_div_r (remainder) + +**Example:** ``DIV r1 r2`` + +11.5. DMEM_WR +------------- + +**Purpose:** Write to Data Memory. + +**Syntax:** ``DMEM_WR [address] source`` + +**Address modes:** +- ``[&imm]`` – literal +- ``[rX]`` – register +- ``[rX + &imm]`` – indexed literal +- ``[rX + rY]`` – indexed register + +**Source:** ``imm #value`` or ``op -op(...)`` + +**Example:** ``DMEM_WR [&10] imm #42`` + +11.6. DPORT_RD +-------------- + +**Purpose:** Read input port. + +**Syntax:** ``DPORT_RD port`` + +**Result:** s_port_l (lower 32 bits), s_port_h (upper 32 bits) + +**Sets:** PORT_DT_NEW flag for that port (cleared by read or CLEAR port) + +**Example:** ``DPORT_RD p0`` + +11.7. DPORT_WR +-------------- + +**Purpose:** Write 32‑bit data to output port. + +**Syntax:** ``DPORT_WR port source [time]`` + +**Source:** ``imm #value`` or ``reg rX`` + +**Time:** ``@imm`` or ``@s_out_time`` (default = s_out_time) + +**Example:** ``DPORT_WR p0 imm 1 @1000`` + +11.8. FLAG +---------- + +**Purpose:** Modify internal flag. + +**Syntax:** ``FLAG operation`` + +**Operations:** ``set``, ``clr``, ``inv`` + +**Example:** ``FLAG set`` + +11.9. JUMP +---------- + +**Purpose:** Conditional or unconditional branch. + +**Syntax:** ``JUMP address -if(condition)`` + +**Address:** label, ``HERE``, ``PREV``, ``NEXT``, ``SKIP``, immediate, or s_addr + +**Latency:** 2 cycles if taken + +**Example:** ``JUMP LOOP -if(NZ)`` + +11.10. NOP +---------- + +**Purpose:** No operation. + +**Syntax:** ``NOP`` + +**Latency:** 1 cycle + +11.11. REG_WR +------------- + +**Purpose:** Write to register (r, s, or w). + +**Syntax:** ``REG_WR destination source`` + +**Source:** ``imm #value``, ``op -op(...)``, ``dmem [addr]``, ``wmem [addr]``, ``label address`` + +**Example:** ``REG_WR r0 imm #100`` + +11.12. RET +---------- + +**Purpose:** Return from subroutine. + +**Syntax:** ``RET`` + +**Latency:** 2 cycles + +11.13. TEST +----------- + +**Purpose:** Perform ALU operation and update flags without writing a register. + +**Syntax:** ``TEST -op(operation) -uf`` + +**Example:** ``TEST -op(r0 - #1) -uf`` + +11.14. TIME +----------- + +**Purpose:** Control time counters. + +**Syntax:** ``TIME operation [value]`` + +**Operations:** ``rst``, ``set_ref``, ``inc_ref``, ``updt`` + +**Example:** ``TIME rst`` + +11.15. TRIG +----------- + +**Purpose:** Set or clear trigger output. + +**Syntax:** ``TRIG port operation [time]`` + +**Operation:** ``set`` or ``clr`` + +**Time:** ``@imm`` or ``@s_out_time`` + +**Example:** ``TRIG p0 set @150`` + +11.16. WAIT (macro) +------------------- + +**Purpose:** Wait for condition. + +**Syntax:** ``WAIT condition`` + +**Conditions:** ``time @N``, ``div_rdy``, ``div_dt``, ``qpa_rdy``, ``qpa_dt``, +``port_dt`` -- this is the **complete** list recognized by +``Instruction.WAIT()`` in ``tprocv2_assembler.py``; anything else +(including ``arith_rdy``/``arith_dt``) raises ``RuntimeError: No +Recognized Operation``. See :ref:`5.10.3 ` for how +to wait on ARITH by hand. + +**Example:** ``WAIT time @1000`` + +11.17. WMEM_WR +-------------- + +**Purpose:** Write r_wave to Wave Parameter Memory. + +**Syntax:** ``WMEM_WR [address]`` + +**Address:** ``[&imm]`` or ``[rX]`` + +**Example:** ``WMEM_WR [&5]`` + +11.18. WPORT_WR +--------------- + +**Purpose:** Write waveform to wave port. + +**Syntax:** ``WPORT_WR port source [time]`` + +**Source:** ``r_wave`` or ``wmem [addr]`` + +**Example:** ``WPORT_WR p1 r_wave @1000`` + +------------------------------------------------------------------------------- + +12. Python API Reference +======================== + +12.1. Initializing the tProcessor +--------------------------------- + +.. code-block:: python + + from qick import QickSoc + from qick.tprocv2_assembler import Assembler + + # Connect to the RFSoC board + soc = QickSoc() + + # Access the tProcessor instance + tproc = soc.tproc + +12.2. Loading a Program +----------------------- + +**From an assembly string:** + +.. code-block:: python + + asm_code = \"\"\" + .ADDR 0x00 + REG_WR r0 imm #100 + DPORT_WR p0 reg r0 @1000 + .END + \"\"\" + prog_bin = Assembler.str_asm2bin(asm_code) + tproc.load_bin_program(prog_bin, load_mem=True) + +**From a list of instructions (dictionary format):** + +.. code-block:: python + + prog_list = [ + {'CMD': 'REG_WR', 'DST': 'r0', 'SRC': 'imm', 'LIT': 100}, + {'CMD': 'DPORT_WR', 'DST': '0', 'SRC': 'reg', 'DATA': 'r0', 'TIME': 1000}, + ] + prog_bin = Assembler.list2bin(prog_list) + tproc.load_bin_program(prog_bin, load_mem=True) + +12.3. Controlling Execution +--------------------------- + +.. code-block:: python + + tproc.time_reset() # Reset time and core + tproc.start() # Start execution + tproc.stop() # Stop execution + tproc.core_start() # Reset and start only core (time continues) + tproc.core_stop() # Stop only core + +12.4. Reading Status and Data +----------------------------- + +.. code-block:: python + + # Human-readable status/debug dumps (see §13.8 for details) -- these are + # the recommended way to inspect tProc state; raw single_read/single_write + # exist on the driver but are explicitly marked "Do not use! Use the + # DMA instead" in their own docstrings. + tproc.print_status() # core/time FSM state, *_rdy/*_dt_new flags + tproc.print_debug() # FIFO flags, DIV/ARITH/QNET/QCOM/port/rand values + + # Read from DMEM + data = tproc.read_dmem(addr=0x10, length=100) + + # Write to DMEM + tproc.load_dmem(buff_in=data, addr=0x20) + +12.5. Configuring the LFSR +-------------------------- + +.. code-block:: python + + # Modes: 0=stop, 1=free run, 2=advance on read, 3=advance on write + tproc.set_lfsr_cfg(mode=2) # advance on read + +------------------------------------------------------------------------------- + +13. Python Examples +=================== + +This section provides practical examples of using the tProcessor v2 from Python. +For complete API documentation, see Chapter 12. + +.. note:: + Earlier revisions of this chapter showed a low-level workflow (hand-written + ``WPORT_WR`` assembly strings, manual ``Waveform``/DMEM packing, and + ``soc.avg_bufs[i].set_avg_mode()``/``set_nsamp()``/``get_data()``) that + doesn't match ``qick_lib`` -- those buffer methods don't exist on the + driver, and ``Waveform`` actually lives in ``qick.asm_v2``, not + ``qick.qick_asm``. The examples below use the real, current + :class:`.QickProgram`-based workflow (the same one used throughout + :doc:`/firmware/generators/sg_v6`, :doc:`/firmware/readouts/index`, and :doc:`/firmware/generators/index`), verified against + ``qick.asm_v2``/``qick.qick_asm``/``qick.drivers.tproc`` in this session. + Raw assembly is still shown where it's genuinely the right tool -- true + hardware-timed conditional branching (:ref:`tproc-conditional-feedback`) + can't be expressed any other way -- but even there, the surrounding + Python (loading/running/reading back) uses real, verified calls. + +.. contents:: + :local: + :depth: 2 + +13.1. Basic Python Workflow +--------------------------- + +Normal QICK programs are written as a :class:`.QickProgram` subclass (most +often :class:`.AveragerProgramV2`): declare your generator/readout channels +once in ``_initialize()``, define named pulses, then play them by name in +``_body()``. The framework handles WMEM/PMEM loading, timing, and +reps/rounds for you. + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + class BasicPulse(AveragerProgramV2): + def _initialize(self, cfg): + 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, degrees, -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 plays the pulse once per rep + +13.2. Basic Loopback Demo (Send and Receive Pulse) +--------------------------------------------------- + +This example sends a pulse and captures its raw (decimated) waveform on a +readout channel -- see :doc:`/topics/readout_modes` for accumulated +(``acquire()``) vs. decimated (``acquire_decimated()``) readout. + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + class LoopbackProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_readout(ch=0, length=3.0, freq=100.0, gen_ch=0) + self.declare_gen(ch=0, nqz=1) + self.add_pulse(ch=0, name="pulse0", style="const", + freq=100.0, phase=0, gain=0.5, length=1.0) + + def _body(self, cfg): + self.trigger(ros=[0], t=0) # arm readout channel 0 + self.pulse(ch=0, name="pulse0", t=0) + self.delay_auto(t=1.0) # wait for pulse+readout before looping + + # single shot, decimated (raw waveform) readout + prog = LoopbackProgram(soccfg, reps=1, final_delay=10.0, cfg={}) + iq = prog.acquire_decimated(soc) + print(f"Acquired {len(iq[0])} samples") + +.. _tproc-conditional-feedback: + +13.3. Conditional Feedback Demo +------------------------------- + +Real-time conditional branching -- e.g. active qubit reset, only playing a +pulse if a measurement crossed a threshold -- must happen inside the +tProc's own instruction stream (no Python round-trip is fast enough), but +you write it with high-level Python calls, not hand-assembled instructions: +:meth:`.QickProgramV2.read_and_jump` combines a readout-register read with a +conditional jump, and :meth:`.QickProgramV2.label` marks the jump target. +This condensed example is adapted from the full working version in +:doc:`/tutorials/04_Real_Time_Feedback`. + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + class ActiveResetProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_gen(ch=cfg['gen_ch'], nqz=1) + self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'], + freq=cfg['freq'], gen_ch=cfg['gen_ch']) + + self.add_pulse(ch=cfg['gen_ch'], name="measure_pulse", style="const", + freq=cfg['freq'], phase=0, gain=1.0, length=cfg['pulse_len']) + self.add_pulse(ch=cfg['gen_ch'], name="reset_pulse", style="const", + freq=cfg['freq'], phase=0, gain=1.0, length=cfg['reset_len']) + + def _body(self, cfg): + # Measure + self.pulse(ch=cfg['gen_ch'], name="measure_pulse", t=0) + self.trigger(ros=[cfg['ro_ch']], t=cfg['trig_time']) + self.wait_auto(cfg['read_wait']) + self.delay_auto(cfg['read_wait'] + cfg['extra_delay']) + + # If I >= threshold (qubit measured in excited state), play reset pulse + self.read_and_jump(ro_ch=cfg['ro_ch'], component='I', + threshold=cfg['threshold'], test='>=', label='NO_RESET') + self.pulse(ch=cfg['gen_ch'], name="reset_pulse", t=0) + self.label("NO_RESET") + + config = {'gen_ch': 0, 'ro_ch': 0, 'freq': 500.0, 'trig_time': 0.25, + 'read_wait': 0.1, 'extra_delay': 0.2, 'threshold': 0, + 'ro_len': 0.1, 'pulse_len': 0.025, 'reset_len': 0.05} + + prog = ActiveResetProgram(soccfg, reps=100, final_delay=0.5, cfg=config) + prog.acquire(soc) + +13.4. Frequency Sweep (Chirp) +----------------------------- + +A frequency sweep is a loop level (:meth:`.QickProgramV2.add_loop`) with a +pulse parameter set to :func:`.QickSweep1D`, rather than a manually +incremented register -- the tProc itself steps the frequency each +iteration, hardware-timed. + +.. code-block:: python + + from qick import * + + soc = QickSoc() + + class ChirpProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_gen(ch=0, nqz=1) + self.add_loop("freq_loop", cfg['steps']) + self.add_pulse(ch=0, name="chirp", style="const", + freq=QickSweep1D("freq_loop", cfg['start_freq'], cfg['end_freq']), + phase=0, gain=0.5, length=cfg['pulse_len']) + + def _body(self, cfg): + self.pulse(ch=0, name="chirp", t=0) + self.delay_auto(t=0.5) + + cfg = {'steps': 100, 'start_freq': 50.0, 'end_freq': 150.0, 'pulse_len': 0.1} + prog = ChirpProgram(soccfg, reps=1, final_delay=1.0, cfg=cfg) + prog.acquire(soc) + +13.5. Complete Example: Send Pulse and Capture Echo +---------------------------------------------------- + +This example combines everything: DAC pulse, ADC capture, and data analysis. + +.. code-block:: python + + from qick import * + import numpy as np + import matplotlib.pyplot as plt + + soc = QickSoc() + + class EchoProgram(AveragerProgramV2): + def _initialize(self, cfg): + self.declare_gen(ch=0, nqz=1) + self.declare_readout(ch=0, length=cfg['ro_len'], freq=cfg['freq'], gen_ch=0) + self.add_pulse(ch=0, name="probe", style="const", + freq=cfg['freq'], phase=0, gain=cfg['gain'], length=cfg['pulse_len']) + + def _body(self, cfg): + self.trigger(ros=[0], t=0) + self.pulse(ch=0, name="probe", t=0) + self.delay_auto(t=1.0) + + cfg = {'freq': 50.0, 'gain': 0.5, 'pulse_len': 1.0, 'ro_len': 2.0} + prog = EchoProgram(soccfg, reps=1, final_delay=10.0, cfg=cfg) + iq = prog.acquire_decimated(soc) # raw waveform for one readout channel + + # --- Analyze --- + data_i, data_q = iq[0][:, 0], iq[0][:, 1] + amplitude = np.sqrt(data_i**2 + data_q**2) + + plt.plot(prog.get_time_axis(ro_index=0), amplitude) + plt.xlabel("Time (us)") + plt.ylabel("Amplitude") + plt.title("Pulse Echo") + plt.show() + + print(f"Peak amplitude: {np.max(amplitude)}") + print(f"Noise floor: {np.mean(amplitude[:100])}") + +13.6. Integration with Signal Generator v6 +------------------------------------------ + +The Signal Generator v6 (SG-v6) is one of several waveform generation +cores QICK supports for DAC outputs -- see :doc:`/firmware/generators/index` for the full +family and :doc:`/firmware/index` for how many channels of which type your +specific board has (it varies by board, see :ref:`std-firmware-per-board`). + +For complete documentation and worked Python examples (basic pulse, shaped +envelope, queued pulses), see :doc:`/firmware/generators/sg_v6`'s "Python Usage" section -- +they use the same :meth:`.QickProgram.declare_gen`/ +:meth:`.QickProgram.add_pulse`/:meth:`.QickProgram.pulse` pattern as every +other example in this chapter. + +13.7. Integration with Readout System +------------------------------------- + +The readout system captures ADC data, triggered by a dedicated single-bit +``trig_N_o`` pin per buffer instance (see :ref:`tproc-ports` in +:doc:`/firmware/index` for the full pin model -- it is *not* a bit within a +channel-numbered data word). QICK supports several readout variants +(single-tone, tProc-configured, and channelized/PFB) -- see :doc:`/firmware/readouts/index` +for the family overview and :ref:`std-firmware-per-board` for which +variant(s) your board uses. + +For the normal workflow (:meth:`.QickProgram.declare_readout`, +:meth:`.QickProgramV2.trigger`, :meth:`.AcquireMixin.acquire`/ +:meth:`.AcquireMixin.acquire_decimated`), see :doc:`/firmware/readouts/index`'s "Python +Interface" section, or the worked examples earlier in this chapter (all of +which declare and trigger a readout channel the same way). + +13.8. Debugging and Monitoring +------------------------------ + +.. note:: + Earlier revisions of this section built examples around + ``soc.tproc.single_read``/``single_write`` -- these exist on the driver + (``qick.drivers.tproc.Axis_QICK_Proc``) but their own docstrings say + **"Do not use! Use the DMA instead"**, and the code has a real bug (an + undefined variable). Use the methods below instead. + +**Reading tProc status and debug state from Python:** + +``soc.tproc`` (:class:`.Axis_QICK_Proc`) has two built-in, human-readable +dumps -- no manual register decoding needed: + +.. code-block:: python + + soc.tproc.print_status() + # prints core/time FSM state, enable bits, all the *_dt_new/*_rdy flags + # (div, arith, qnet, qcom, qp1, qp2), decoded from the status register + + soc.tproc.print_debug() + # prints FIFO empty/full flags per peripheral, plus DIV/ARITH/QNET/QCOM/ + # QPA/QPB/port/rand debug read-back values + +You can also print the board/processor configuration once, up front, as a +sanity check before debugging further: + +.. code-block:: python + + soc.tproc.info() # prints instantiated parameters (memory sizes, + # which optional peripherals are present, clocks) + print(soccfg) # full board config: every generator/readout/buffer + +13.9. Performance Measurement +----------------------------- + +**Wall-clock timing from Python** (includes PS-PL DMA and Python overhead, +not just tProc execution time -- useful for overall experiment throughput): + +.. code-block:: python + + import time + + start = time.perf_counter() + iq = prog.acquire(soc, progress=False) + print(f"acquire() took {time.perf_counter() - start:.3f} s") + +**Hardware-precise timing with an oscilloscope**, using a spare PMOD +trigger pin bracketing the section of interest -- this measures actual +tProc/firmware timing, unaffected by PS-side overhead: + +.. code-block:: text + + ; in raw assembly, or via QickProgram's pins= argument to trigger() + TRIG p0 set @start_time + ; ... section to measure ... + TRIG p0 clr @end_time + +13.10. Repository of Examples +----------------------------- + +This documentation's own tutorial notebooks (:doc:`/tutorials/README`) are +the primary worked-example resource, all runnable on real hardware (or, for +tutorials 00-07, on the :doc:`/tutorials/README`'s Colab-runnable emulator +port): + +.. list-table:: Tutorial Notebooks Covering This Chapter's Topics + :header-rows: 1 + :widths: 45 55 + + * - Notebook + - Covers + * - :doc:`/tutorials/01_Basic_Sequencing` + - Basic pulses, sequences, acquisition (§13.1-13.2) + * - :doc:`/tutorials/04_Real_Time_Feedback` + - Conditional pulses, active reset, thresholding (§13.3) + * - :doc:`/tutorials/02_Parameter_Sweeps` + - 1D/2D sweeps with ``QickSweep1D``/``add_loop`` (§13.4) + * - :doc:`/tutorials/06_Generators_And_Readouts` + - Generators, readouts, I/Q mixing (§13.5-13.7) + +13.11. Where to Find More Examples +---------------------------------- + +For more elaborate examples (multi-qubit experiments, advanced feedback +loops, calibration routines), refer to: + +- **Tutorial series** (:doc:`/tutorials/README`) – The full series, + from basic sequencing through streaming/DSP, custom firmware + integration, and XCOM multi-board network synchronization. +- **Official QICK Demos Repository** – + `github.com/openquantumhardware/qick/tree/main/qick_demos `_. +- **Community Contributions** – Check the ``#qick`` channel on the + `Unitary Fund Discord `_ for + user-contributed examples and support. +- **QICK Papers** – See :doc:`/papers` for academic papers that include + example use cases and code snippets. + +------------------------------------------------------------------------------- + +.. _tproc-pitfalls: + +14. Common Pitfalls and Debugging +================================== + +14.1. Pitfall: Forgetting to Wait for Peripherals +------------------------------------------------- + +**Wrong:** + +.. code-block:: text + + ARITH T r1 r2 + REG_WR r3 op -op(s_arith_low) // reads stale data! + +**Correct:** + +.. code-block:: text + + ARITH T r1 r2 + TEST -op(s_status AND #2) -uf // no WAIT macro for arith_dt -- poll by hand + JUMP HERE -if(Z) + REG_WR r3 op -op(s_arith_low) + +14.2. Pitfall: FIFO Overflow +---------------------------- + +**Symptom:** Program stalls unexpectedly. + +**Check:** ``s_status[15]`` (FIFO_FULL) + +**Fix:** Increase FIFO depth in hardware, or add delays between port writes. + +14.3. Pitfall: Branch Penalty in Tight Loops +-------------------------------------------- + +**Symptom:** Loop takes longer than expected. + +**Example of inefficient loop:** + +.. code-block:: text + + LOOP: + // ... code ... + JUMP LOOP -if(NZ) // 2 cycle penalty each iteration + +**Better: use conditional execution or unroll.** + +14.4. Pitfall: Using s_out_time Incorrectly +-------------------------------------------- + +**Wrong:** + +.. code-block:: text + + WPORT_WR p0 r_wave @1000 + WPORT_WR p0 r_wave @2000 // second write uses immediate 2000 + +**Correct (using s_out_time for incremental timing):** + +.. code-block:: text + + REG_WR s_out_time imm #1000 + WPORT_WR p0 r_wave @s_out_time + REG_WR s_out_time op -op(s_out_time + #1000) + WPORT_WR p0 r_wave @s_out_time + +14.5. Debugging Technique: Single Step +--------------------------------------- + +From Python: + +.. code-block:: python + + soc.tproc.core_step() # execute one instruction, then stop + +You can single‑step through your program to see where it gets stuck. + +14.6. Debugging Technique: Read Debug Register +---------------------------------------------- + +.. code-block:: python + + soc.tproc.print_status() # core/time FSM state, enable bits, *_rdy/*_dt_new flags + soc.tproc.print_debug() # FIFO empty/full flags, DIV/ARITH/QNET/QCOM/port/rand values + +See §13.8 ("Debugging and Monitoring") for details on these two methods -- +they replace manual register decoding via ``single_read``, which the +driver itself flags as "Do not use". + +14.7. Common Issues Quick Reference +------------------------------------ + +.. list-table:: + :header-rows: 1 + :widths: 40 55 + + * - Symptom + - Likely cause and fix + * - Output never appears on port + - FIFO full (check ``s_status[15]``); or ``abs_time`` already past ``out_abs_time`` + * - Flag condition never triggers + - Condition flag set in instruction N but tested in N+1 (2-cycle lag). Insert NOPs. + * - DIV/ARITH results wrong + - Reading result before ``_DT_NEW`` is set. Use ``WAIT div_dt`` for DIV; + for ARITH there's no ``WAIT`` macro, poll ``s_status`` bit 1 by hand + (:ref:`5.10.3 `). + * - Jump goes to wrong place + - Using ``[]`` around a label: ``JUMP [LABEL]`` is wrong; use ``JUMP LABEL``. + * - Time does not advance + - ``TIME`` instructions issued < 5 cycles apart. Insert NOPs. + * - ``s_addr``-based JUMP misbehaves + - Address written to ``s15`` too close to JUMP. Insert at least 2 NOPs after writing ``s15``. + +------------------------------------------------------------------------------- + +15. FAQ (Frequently Asked Questions) +==================================== + +**Q1: Can I use floating‑point numbers?** +A: No. The tProc only supports integer arithmetic. Pre‑compute floats in Python and convert to fixed‑point (e.g., scale by 2^16). + +**Q2: What is the maximum program size?** +A: Up to 65536 instructions (64 bits each) if configured with PMEM_AW=16. Default is 1024 instructions. + +**Q3: How accurate is the timing?** +A: The dispatcher compares times every t_clk cycle. Jitter is ±1 t_clk cycle (~2.6 ns at 384 MHz). The 5‑cycle write‑to‑output latency is deterministic. + +**Q4: Can I interrupt the tProc?** +A: Not in the current version. The tProc runs to completion or until stopped by Python. + +**Q5: How do I synchronize two tProcessors on different boards?** +A: Use the SYNC signal (abs_time bit 29) or external triggers. Distribute a common reference clock. + +**Q6: What happens when time overflows (48 bits)?** +A: Time rolls over to 0. For long experiments, reset time periodically or use relative timing. + +**Q7: Can I use a register as a time value in @?** +A: The assembler supports ``@rX`` but this manual recommends using s_out_time for clarity. + +**Q8: Why does my program get stuck on WAIT div_dt?** +A: You may have forgotten to start a division (DIV) before waiting. Or the division completed long ago and you need to clear the flag. + +**Q9: How do I generate a sine wave?** +A: The signal generator (separate block) generates sine/cosine from DDS. The tProc only sends frequency/phase/gain parameters. + +**Q10: Can I read the same input port multiple times?** +A: Yes. Each read clears the PORT_DT_NEW flag for that port. New data will set it again. + +------------------------------------------------------------------------------- + +16. Glossary +============ + +.. list-table:: + :header-rows: 1 + + * - Term + - Definition + * - ALU + - Arithmetic Logic Unit. Performs integer operations. + * - AXI‑Lite + - Simple AXI protocol for register access. + * - AXI‑Stream + - Streaming AXI protocol for high‑bandwidth data. + * - CDC + - Clock Domain Crossing. Transferring data between different clock domains. + * - DDS + - Direct Digital Synthesizer. Generates sine/cosine waves. + * - Dispatcher + - Hardware block that compares time and updates outputs. + * - DMA + - Direct Memory Access. Transfers data without CPU involvement. + * - DMEM + - Data Memory. 32‑bit wide, for user data. + * - DSP48 + - FPGA DSP slice. Used for multiplication. + * - FIFO + - First‑In First‑Out queue. Holds scheduled outputs. + * - LFSR + - Linear Feedback Shift Register. Generates pseudo‑random numbers. + * - Pipeline + - Series of stages in the CPU. Allows overlapping instruction execution. + * - PMEM + - Program Memory. 64‑bit wide, stores instructions. + * - PRNG + - Pseudo‑Random Number Generator. See LFSR. + * - PS + - Processing System (ARM processor in the FPGA). + * - tProc + - timing Processor. Subject of this manual. + * - WMEM + - Wave Memory. 168‑bit wide, stores waveform parameters. + +------------------------------------------------------------------------------- + +*End of Document* diff --git a/docs/source/tutorials/00_Getting_Started.ipynb b/docs/source/tutorials/00_Getting_Started.ipynb new file mode 100644 index 000000000..bd3c09506 --- /dev/null +++ b/docs/source/tutorials/00_Getting_Started.ipynb @@ -0,0 +1,353 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 00 - Getting Started with QICK and tProc v2\n", + "\n", + "**Objective:** Verify your QICK installation, connect to the RFSoC board, and understand the basic hardware configuration." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup and Imports" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QICK Version: 0.2.381\n" + ] + } + ], + "source": [ + "# Jupyter notebook setup\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "# Core QICK imports\n", + "from qick import *\n", + "\n", + "# tProc v2 specific classes\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D, QickSpan\n", + "\n", + "# Optional: increase logging level for debugging\n", + "# logging.basicConfig(level=logging.DEBUG)\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "# Reduce noise from the tProc core\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Display the loaded QICK library version\n", + "print(f\"QICK Version: {qick.get_version()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:** This cell imports all necessary libraries. The `qick.asm_v2` module contains the classes for building tProc v2 programs. The logging configuration helps with debugging if something goes wrong." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Connect to the RFSoC Board\n", + "\n", + "You need to provide the correct path to your firmware `.bit` file. This file is specific to your board model and the firmware version." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== Board Connection Successful ===\n" + ] + } + ], + "source": [ + "# --- IMPORTANT: Change this path to point to your firmware file ---\n", + "# Common paths for different boards (examples):\n", + "# ZCU111: '/home/xilinx/jupyter_notebooks/qick_firmware/out/qick_zcu111.bit'\n", + "# RFSoC4x2: '/home/xilinx/jupyter_notebooks/qick_firmware/out/qick_4x2.bit'\n", + "# ZCU216: '/home/xilinx/jupyter_notebooks/qick_firmware/out/qick_216.bit'\n", + "\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- YOU MUST CHANGE THIS\n", + "\n", + "# Connect to the board\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc # 'soccfg' is a common alias used throughout QICK examples\n", + "\n", + "print(\"\\n=== Board Connection Successful ===\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Inspect the Hardware Configuration\n", + "\n", + "The `QickSoc` object contains all the information about your firmware: clock frequencies, generator channels, readout channels, digital I/O, and tProc capabilities." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "QICK running on ZCU216, software version 0.2.381\n", + "\n", + "Firmware configuration (built Tue Sep 3 12:55:04 2024):\n", + "\n", + "\tGlobal clocks (MHz): tProc dispatcher timing 614.400, RF reference 245.760\n", + "\tGroups of related clocks: [tProc timing clock, DAC tile 2], [DAC tile 3], [ADC tile 2]\n", + "\n", + "\t5 signal generator channels:\n", + "\t0:\taxis_signal_gen_v6 - fs=9830.400 Msps, fabric=614.400 MHz\n", + "\t\tenvelope memory: 16384 complex samples (1.667 us)\n", + "\t\t32-bit DDS, range=9830.400 MHz\n", + "\t\tDAC tile 2, blk 0 is 0_230 on JHC3, or QICK box DAC port 8\n", + "\t1:\taxis_sg_int4_v2 - fs=6881.280 Msps, fabric=430.080 MHz\n", + "\t\tenvelope memory: 4096 complex samples (9.524 us)\n", + "\t\t32-bit DDS, range=1720.320 MHz\n", + "\t\tDAC tile 3, blk 0 is 0_231 on JHC3, or QICK box DAC port 12\n", + "\t2:\taxis_sg_int4_v2 - fs=6881.280 Msps, fabric=430.080 MHz\n", + "\t\tenvelope memory: 4096 complex samples (9.524 us)\n", + "\t\t32-bit DDS, range=1720.320 MHz\n", + "\t\tDAC tile 3, blk 1 is 1_231 on JHC4, or QICK box DAC port 13\n", + "\t3:\taxis_sg_mux8_v1 - fs=9830.400 Msps, fabric=614.400 MHz\n", + "\t\t32-bit DDS, range=9830.400 MHz\n", + "\t\tDAC tile 2, blk 2 is 2_230 on JHC3, or QICK box DAC port 10\n", + "\t4:\taxis_sg_mixmux8_v1 - fs=6881.280 Msps, fabric=430.080 MHz\n", + "\t\t32-bit DDS, range=1720.320 MHz\n", + "\t\tDAC tile 3, blk 2 is 2_231 on JHC3, or QICK box DAC port 14\n", + "\n", + "\t7 readout channels:\n", + "\t0:\taxis_dyn_readout_v1 - configured by tProc output 3\n", + "\t\tfs=2457.600 Msps, decimated=307.200 MHz, 32-bit DDS, range=2457.600 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (3.333 us)\n", + "\t\ttriggered by tport 0, pin 0, feedback to tProc input 0\n", + "\t\tADC tile 2, blk 0 is 0_226 on JHC7, or QICK box ADC port 4\n", + "\t1:\taxis_readout_v3 - configured by tProc output 3\n", + "\t\tfs=2457.600 Msps, decimated=614.400 MHz, 32-bit DDS, range=2457.600 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (1.667 us)\n", + "\t\ttriggered by tport 1, pin 0, feedback to tProc input 1\n", + "\t\tADC tile 2, blk 1 is 1_226 on JHC8, or QICK box ADC port 5\n", + "\t2:\taxis_pfb_readout_v3 - configured by PYNQ\n", + "\t\tfs=2457.600 Msps, decimated=38.400 MHz, 32-bit DDS, range=38.400 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (26.667 us)\n", + "\t\ttriggered by tport 2, pin 0, feedback to tProc input 2\n", + "\t\tADC tile 2, blk 2 is 2_226 on JHC7, or QICK box ADC port 6\n", + "\t3:\taxis_pfb_readout_v3 - configured by PYNQ\n", + "\t\tfs=2457.600 Msps, decimated=38.400 MHz, 32-bit DDS, range=38.400 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (26.667 us)\n", + "\t\ttriggered by tport 3, pin 0, feedback to tProc input 3\n", + "\t\tADC tile 2, blk 2 is 2_226 on JHC7, or QICK box ADC port 6\n", + "\t4:\taxis_pfb_readout_v3 - configured by PYNQ\n", + "\t\tfs=2457.600 Msps, decimated=38.400 MHz, 32-bit DDS, range=38.400 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (26.667 us)\n", + "\t\ttriggered by tport 4, pin 0, feedback to tProc input 4\n", + "\t\tADC tile 2, blk 2 is 2_226 on JHC7, or QICK box ADC port 6\n", + "\t5:\taxis_pfb_readout_v3 - configured by PYNQ\n", + "\t\tfs=2457.600 Msps, decimated=38.400 MHz, 32-bit DDS, range=38.400 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (26.667 us)\n", + "\t\ttriggered by tport 5, pin 0, feedback to tProc input 5\n", + "\t\tADC tile 2, blk 2 is 2_226 on JHC7, or QICK box ADC port 6\n", + "\t6:\taxis_readout_v2 - configured by PYNQ\n", + "\t\tfs=2457.600 Msps, decimated=307.200 MHz, 32-bit DDS, range=2457.600 MHz\n", + "\t\taxis_avg_buffer v1.0 (no edge counter, no weights)\n", + "\t\tmemory 16384 accumulated, 1024 decimated (3.333 us)\n", + "\t\ttriggered by tport 6, pin 0, feedback to tProc input 6\n", + "\t\tADC tile 2, blk 3 is 3_226 on JHC8, or QICK box ADC port 7\n", + "\n", + "\t8 digital output pins:\n", + "\t0:\tPMOD0_0_LS\n", + "\t1:\tPMOD0_1_LS\n", + "\t2:\tPMOD0_2_LS\n", + "\t3:\tPMOD0_3_LS\n", + "\t4:\tPMOD0_4_LS\n", + "\t5:\tPMOD0_5_LS\n", + "\t6:\tPMOD0_6_LS\n", + "\t7:\tPMOD0_7_LS\n", + "\n", + "\ttProc: qick_processor (\"v2\") rev 21, core execution clock 200.000 MHz\n", + "\t\tmemories (words): program 1024, data 1024, waveform 256\n", + "\t\texternal start pin: PMOD1_0_LS\n", + "\t\texternal stop pin: PMOD1_1_LS\n", + "\n", + "\tDDR4 memory buffer: 1073741824 samples (3.495 sec), 128 samples/transfer\n", + "\t\twired to readouts [0, 2, 3, 4, 5, 6]\n", + "\n", + "\tMR buffer: 8192 samples (3.333 us), wired to readouts [0, 6]\n" + ] + } + ], + "source": [ + "print(soccfg)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:** The printed output shows:\n", + "- **Global clocks:** tProc timing clock, RF reference frequency\n", + "- **Signal generators:** Number of channels, sample rates, envelope memory size\n", + "- **Readout channels:** Sample rates, buffer sizes, trigger mappings\n", + "- **Digital output pins:** Available PMOD pins\n", + "- **tProc configuration:** Program memory size, data memory size, waveform memory size, core clock\n", + "\n", + "This information is essential when configuring your experiments." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Quick Hardware Test\n", + "\n", + "To verify that the board is responding correctly, read the tProc's program counter and test data memory access." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "tProc Program Counter: 0\n", + "Data memory read test: [0]\n", + "\n", + "✓ Basic board communication verified via program counter read.\n" + ] + } + ], + "source": [ + "# Read the tProc's program counter (should be 0 after reset)\n", + "pc = soc.get_tproc_counter(addr=1)\n", + "print(f\"tProc Program Counter: {pc}\")\n", + "\n", + "# Read a data memory address to verify communication\n", + "# Data memory address 0 should contain some value, but reading it confirms the interface works\n", + "try:\n", + " data = soc.read_mem(length=1, mem_sel='dmem', addr=0)\n", + " print(f\"Data memory read test: {data}\")\n", + "except Exception as e:\n", + " print(f\"Data memory read failed: {e}\")\n", + "\n", + "print(\"\\n✓ Basic board communication verified via program counter read.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:** These simple reads confirm that the communication with the board is working correctly. The tProc program counter should be 0 before we load a program." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Summary\n", + "\n", + "You are now ready to start writing QICK experiments. The next notebook ([``01_Basic_Sequencing.ipynb``](./01_Basic_Sequencing.ipynb)) will show you how to define and play pulses, and acquire data.\n", + "\n", + "**Key takeaways:**\n", + "- Use `QickSoc(path)` to connect to your board\n", + "- The `soccfg` object holds all hardware configuration information\n", + "- Always check that your firmware path is correct for your specific board\n", + "- Use `get_tproc_counter()` and `read_mem()` to verify board communication" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/01_Basic_Sequencing.ipynb b/docs/source/tutorials/01_Basic_Sequencing.ipynb new file mode 100644 index 000000000..d0927b6ba --- /dev/null +++ b/docs/source/tutorials/01_Basic_Sequencing.ipynb @@ -0,0 +1,497 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 01 - Basic Sequencing with tProc v2\n", + "\n", + "**Objective:** Learn how to define pulses, create a sequence, acquire data, and understand the basic structure of a tProc v2 program." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels (adjust based on your firmware)\n", + "GEN_CH = 0 # Generator channel 0\n", + "RO_CH = 0 # Readout channel 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Your First Program: The Structure\n", + "\n", + "Every tProc v2 program follows this pattern:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class MyFirstProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " \"\"\"Setup: Called once when the program is compiled.\"\"\"\n", + " # 1. Declare hardware resources\n", + " # 2. Define envelopes and pulses\n", + " # 3. Configure readout\n", + " pass\n", + "\n", + " def _body(self, cfg):\n", + " \"\"\"Sequence: Called for each repetition (shot).\"\"\"\n", + " # 1. Play pulses\n", + " # 2. Trigger readout\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The key distinction:\n", + "- **`_initialize()`** runs during **compilation** (Python side, once)\n", + "- **`_body()`** runs during **execution** (tProc side, many times)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. A Simple Single-Pulse Program\n", + "\n", + "Let's create a program that plays a single constant pulse and measures it." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7adfd5d528074f1d98d29b424532fa4f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class SinglePulseProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " # Declare the generator and readout channels\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1) # nqz=1 means no frequency folding\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define a constant pulse (no envelope shaping)\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=\"my_pulse\",\n", + " style=\"const\", # Constant amplitude\n", + " freq=cfg['freq'], # Frequency in MHz\n", + " length=cfg['pulse_len'], # Duration in microseconds\n", + " phase=cfg['phase'], # Phase in degrees\n", + " gain=cfg['gain'] # Amplitude (0 to 1)\n", + " )\n", + "\n", + " # Configure the readout\n", + " self.add_readoutconfig(\n", + " ch=cfg['ro_ch'],\n", + " name=\"my_ro\",\n", + " freq=cfg['freq'],\n", + " gen_ch=cfg['gen_ch']\n", + " )\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " # Play the pulse at time t=0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"my_pulse\", t=0)\n", + " # Trigger the readout\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Configure the experiment\n", + "config = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0, # MHz\n", + " 'pulse_len': 0.1, # microseconds\n", + " 'phase': 0, # degrees\n", + " 'gain': 1.0, # full amplitude\n", + " 'trig_time': 0.2, # microseconds (when to start readout)\n", + " 'ro_len': 0.5 # microseconds (readout duration)\n", + "}\n", + "\n", + "# Create and run the program\n", + "prog = SinglePulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "\n", + "# Acquire data (decimated mode gives IQ traces vs time)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10) # 10 averages\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot the results\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I (In-phase)')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q (Quadrature)')\n", + "plt.plot(time_axis, np.abs(iq_data[0].dot([1,1j])), label='Magnitude')\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('Single Pulse Measurement')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation of the cell:**\n", + "- `declare_gen()` and `declare_readout()` tell the tProc which hardware resources we'll use\n", + "- `add_pulse()` defines a pulse with its parameters. A constant pulse (`style=\"const\"`) has flat amplitude.\n", + "- In `_body()`, `pulse()` schedules the pulse, and `trigger()` starts the readout acquisition\n", + "- `acquire_decimated()` returns averaged IQ data. The `rounds` parameter controls how many times the entire experiment is repeated for averaging." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Multiple Pulses in One Sequence\n", + "\n", + "You can define many pulses in `_initialize()` and play them in any order." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "70174dbcae064fab8d6b1a03dc8d8728", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class MultiPulseProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define a Gaussian envelope\n", + " ramp_len = 0.2\n", + " self.add_gauss(\n", + " ch=cfg['gen_ch'],\n", + " name=\"gauss_env\",\n", + " sigma=ramp_len/10,\n", + " length=ramp_len,\n", + " even_length=True\n", + " )\n", + "\n", + " # Define different pulse types\n", + " # Flat-top pulse (Gaussian rise/fall, constant middle)\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"p_flattop\",\n", + " style=\"flat_top\", envelope=\"gauss_env\",\n", + " freq=cfg['freq'], length=0.1, phase=0, gain=1.0\n", + " )\n", + "\n", + " # Gaussian pulse\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"p_gauss\",\n", + " style=\"arb\", envelope=\"gauss_env\",\n", + " freq=cfg['freq'], phase=0, gain=1.0\n", + " )\n", + "\n", + " # Constant pulse\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"p_const\",\n", + " style=\"const\",\n", + " freq=cfg['freq'], length=0.15, phase=0, gain=1.0\n", + " )\n", + "\n", + " # Flat-top pulse with 90-degree phase\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"p_flattop_90\",\n", + " style=\"flat_top\", envelope=\"gauss_env\",\n", + " freq=cfg['freq'], length=0.1, phase=90, gain=1.0\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + "\n", + " # Sequence of pulses at different times (microseconds)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"p_flattop\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"p_gauss\", t=0.4)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"p_const\", t=0.8)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"p_flattop_90\", t=1.2)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"p_gauss\", t=1.6)\n", + "\n", + "# Run the multi-pulse sequence\n", + "config['ro_len'] = 2.0 # Longer readout to capture all pulses\n", + "prog = MultiPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q')\n", + "plt.plot(time_axis, np.abs(iq_data[0].dot([1,1j])), label='Magnitude')\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('Multi-Pulse Sequence')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Key observations from this example:**\n", + "- Once a pulse is defined, it can be reused multiple times (`p_gauss` appears twice)\n", + "- Different pulse styles serve different purposes: `const` for square pulses, `arb` for shaped pulses, `flat_top` for shaped pulses with a constant top\n", + "- The `even_length=True` parameter ensures the envelope has an even number of samples, which is required for some operations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Inspecting Your Program\n", + "\n", + "You can print the program to see what the tProc will execute:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "macros:\n", + "\tWriteReg(dst='s_core_w1', src=0)\n", + "\tConfigReadout(ch=0, name='my_ro', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tDelay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 614})\n", + "\tOpenLoop(n=1, name='reps')\n", + "\tTrigger(ros=[0], tts=[], pins=[0], t=0.2, width=0.016276041666666668, ddr4=True, mr=False, tag=None, t_params={'t': , 'width': }, t_regs={'t': 123, 'width': 10}, outdict=defaultdict(, {0: 1}), trigset={0, 7})\n", + "\tPulse(ch=0, name='p_flattop', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tPulse(ch=0, name='p_gauss', t=0.4, tag=None, t_params={'t': }, t_regs={'t': 246})\n", + "\tPulse(ch=0, name='p_const', t=0.8, tag=None, t_params={'t': }, t_regs={'t': 492})\n", + "\tPulse(ch=0, name='p_flattop_90', t=1.2, tag=None, t_params={'t': }, t_regs={'t': 737})\n", + "\tPulse(ch=0, name='p_gauss', t=1.6, tag=None, t_params={'t': }, t_regs={'t': 983})\n", + "\tWait(t=0, auto=True, gens=False, ros=True, tag=None, no_warn=True, t_params={'t': }, t_regs={'t': 1351})\n", + "\tDelay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 1658})\n", + "\tIncReg(dst='s_core_w1', src=1)\n", + "\tCloseLoop()\n", + "\tEnd()\n", + "registers:\n", + "\treps: QickRegisterV2(addr=0, init=None)\n", + "pulses:\n", + "\tp_flattop: QickPulse(waveforms=[Waveform(name='p_flattop_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=61, conf=8), Waveform(name='p_flattop_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=61, spans={}, quantize=1, steps={}), conf=9), Waveform(name='p_flattop_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=61, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=61, conf=8)])\n", + "\tp_gauss: QickPulse(waveforms=[Waveform(name='p_gauss_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=122, conf=8)])\n", + "\tp_const: QickPulse(waveforms=[Waveform(name='p_const_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=92, spans={}, quantize=1, steps={}), conf=9)])\n", + "\tp_flattop_90: QickPulse(waveforms=[Waveform(name='p_flattop_90_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=61, conf=8), Waveform(name='p_flattop_90_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=61, spans={}, quantize=1, steps={}), conf=9), Waveform(name='p_flattop_90_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=1073741824, spans={}, quantize=1, steps={}), env=61, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=61, conf=8)])\n", + "\tmy_ro: QickPulse(waveforms=[Waveform(name='my_ro_w0', freq=QickRawParam(par='freq', start=-873813332, spans={}, quantize=-4, steps={}), phase=0, env=0, gain=0, length=3, conf=520)])\n", + "expanded ASM:\n", + "\t NOP \n", + "\t REG_WR s12 imm #0 \n", + "\t WPORT_WR p3 wmem [&8] @0 \n", + "\t TIME #614 inc_ref \n", + "\t REG_WR r0 imm #0 \n", + "\treps:\n", + "\t DPORT_WR p0 imm 1 @123 \n", + "\t TRIG p0 set @123 \n", + "\t TRIG p7 set @123 \n", + "\t DPORT_WR p0 imm 0 @133 \n", + "\t TRIG p0 clr @133 \n", + "\t TRIG p7 clr @133 \n", + "\t WPORT_WR p0 wmem [&0] @0 \n", + "\t WPORT_WR p0 wmem [&1] @0 \n", + "\t WPORT_WR p0 wmem [&2] @0 \n", + "\t WPORT_WR p0 wmem [&3] @246 \n", + "\t WPORT_WR p0 wmem [&4] @492 \n", + "\t WPORT_WR p0 wmem [&5] @737 \n", + "\t WPORT_WR p0 wmem [&6] @737 \n", + "\t WPORT_WR p0 wmem [&7] @737 \n", + "\t WPORT_WR p0 wmem [&3] @983 \n", + "\t WAIT @1351 time \n", + "\t TIME #1658 inc_ref \n", + "\t REG_WR s12 op -op(s12 + #1) \n", + "\t TEST -op(r0 - #0) \n", + "\t JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + "\t JUMP HERE \n" + ] + } + ], + "source": [ + "print(prog)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:** This prints the compiled program in three sections:\n", + "1. **macros:** High-level operations (pulse definitions, loops, triggers)\n", + "2. **registers:** Allocated data registers\n", + "3. **pulses:** Defined waveforms with their parameters\n", + "4. **expanded ASM:** The actual assembly code that runs on the tProc\n", + "\n", + "Understanding this output becomes important when debugging complex sequences." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Summary\n", + "\n", + "You have learned:\n", + "- The structure of a tProc v2 program (`_initialize` vs `_body`)\n", + "- How to define and play pulses of different styles (const, arb, flat_top)\n", + "- How to trigger readout and acquire IQ data\n", + "- How to inspect the generated assembly code\n", + "\n", + "**Next steps:** Proceed to [``02_Parameter_Sweeps.ipynb``](./02_Parameter_Sweeps.ipynb) to learn how to automate parameter variation and create 2D scans." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/02_Parameter_Sweeps.ipynb b/docs/source/tutorials/02_Parameter_Sweeps.ipynb new file mode 100644 index 000000000..8d441b892 --- /dev/null +++ b/docs/source/tutorials/02_Parameter_Sweeps.ipynb @@ -0,0 +1,574 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 02 - Parameter Sweeps with tProc v2\n", + "\n", + "**Objective:** Learn how to automate parameter variation (sweeps) to create 1D and 2D scans, and understand how to retrieve the actual swept values." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D, QickSpan\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding Sweeps\n", + "\n", + "In QICK, a **sweep** is an automatic variation of a parameter across multiple experiment shots. The tProc handles the looping internally, eliminating Python-to-FPGA communication latency.\n", + "\n", + "Key components:\n", + "- **`add_loop(name, steps)`** - Defines a loop that will run `steps` times\n", + "- **`QickSweep1D(loop_name, start, stop)`** - Creates a linear sweep from `start` to `stop` over the loop iterations\n", + "- **`get_pulse_param(pulse_name, param, as_array=True)`** - Retrieves the actual swept values (after hardware quantization)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. A Simple 1D Sweep\n", + "\n", + "Let's sweep the phase of a flat-top pulse from -360° to 720° while also sweeping the gain from 0 to 1." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b8d194b06e364eaebdc426464fc03e87", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Swept phase values (degrees): [-360. -90. 180. 450. 720.]\n", + "Swept gain values: [0. 0.24995422 0.49990844 0.74986266 0.99981688]\n" + ] + } + ], + "source": [ + "class SimpleSweepProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define the loop that controls our sweep\n", + " self.add_loop(\"my_sweep\", self.cfg[\"steps\"])\n", + "\n", + " # Define a Gaussian envelope for pulse shaping\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", \n", + " sigma=cfg['ramp_len']/10, \n", + " length=cfg['ramp_len'], \n", + " even_length=True)\n", + " \n", + " # Define the pulse with swept parameters\n", + " self.add_pulse(ch=gen_ch, name=\"swept_pulse\", \n", + " style=\"flat_top\", \n", + " envelope=\"ramp\", \n", + " freq=cfg['freq'], \n", + " length=cfg['flat_len'],\n", + " phase=QickSweep1D(\"my_sweep\", -360, 720), # Sweep from -360° to 720°\n", + " gain=QickSweep1D(\"my_sweep\", 0.0, 1.0) # Sweep gain from 0 to 1\n", + " )\n", + "\n", + " # Configure readout\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"swept_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Configuration\n", + "config = {\n", + " 'steps': 5, # Number of sweep points\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 100, # MHz\n", + " 'trig_time': 0.25, # µs\n", + " 'ro_len': 0.55, # µs\n", + " 'flat_len': 0.05, # µs (flat segment duration)\n", + " 'ramp_len': 0.2 # µs (ramp duration)\n", + "}\n", + "\n", + "# Run the sweep\n", + "prog = SimpleSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot magnitude for each sweep step\n", + "plt.figure(figsize=(10, 6))\n", + "for i, iq_point in enumerate(iq_data[0]):\n", + " plt.plot(time_axis, np.abs(iq_point.dot([1,1j])), label=f\"Step {i}\")\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Magnitude (a.u.)')\n", + "plt.title('1D Sweep: Phase from -360° to 720°, Gain from 0 to 1')\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "# Retrieve the actual swept values\n", + "phases = prog.get_pulse_param('swept_pulse', 'phase', as_array=True)\n", + "gains = prog.get_pulse_param('swept_pulse', 'gain', as_array=True)\n", + "print(f\"Swept phase values (degrees): {phases}\")\n", + "print(f\"Swept gain values: {gains}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- `add_loop(\"my_sweep\", steps)` creates a loop. Any parameter wrapped in `QickSweep1D()` will vary across this loop.\n", + "- The tProc automatically:\n", + " 1. Updates the waveform parameters for each step\n", + " 2. Executes the `_body()` code\n", + " 3. Loops until all steps are complete\n", + "- `get_pulse_param()` returns the **actual** values after quantization, which may differ slightly from the requested values due to hardware precision limits." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. 2D Sweeps with Nested Loops\n", + "\n", + "For multi-dimensional scans, we can nest loops. The order of `add_loop()` calls determines the nesting order (first declared is outermost)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c53fda1a2e7b4dbfbeba4d45abd80a29", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/500000 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class LengthSweepProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " self.add_loop(\"length_sweep\", self.cfg[\"steps\"])\n", + "\n", + " # Gaussian envelope (fixed parameters)\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", \n", + " sigma=cfg['ramp_len']/10, \n", + " length=cfg['ramp_len'], \n", + " even_length=True)\n", + " \n", + " # Flat-top pulse with swept length\n", + " self.add_pulse(ch=gen_ch, name=\"len_pulse\", \n", + " style=\"flat_top\", \n", + " envelope=\"ramp\", \n", + " freq=cfg['freq'], \n", + " length=QickSweep1D(\"length_sweep\", 0.05, 0.15), # Sweep from 50ns to 150ns\n", + " phase=0,\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"len_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Configuration\n", + "config_len = {\n", + " 'steps': 5,\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.5,\n", + " 'ramp_len': 0.1,\n", + " 'gain': 1.0\n", + "}\n", + "\n", + "# Run the length sweep\n", + "prog = LengthSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config_len)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Get the actual length values\n", + "lengths = prog.get_pulse_param('len_pulse', 'length', as_array=True)\n", + "total_lengths = prog.get_pulse_param('len_pulse', 'total_length', as_array=True)\n", + "print(f\"Flat segment lengths (µs): {lengths}\")\n", + "print(f\"Total pulse lengths (µs): {total_lengths}\")\n", + "\n", + "# Plot results\n", + "plt.figure(figsize=(10, 6))\n", + "for i, iq_point in enumerate(iq_data[0]):\n", + " plt.plot(time_axis, np.abs(iq_point.dot([1,1j])), label=f\"Length = {lengths[i]:.3f} µs\")\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Magnitude (a.u.)')\n", + "plt.title('Sweeping Pulse Length')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note on pulse lengths:**\n", + "- `length` refers to the flat segment duration (for flat-top pulses)\n", + "- `total_length` includes the rise and fall times\n", + "- Both can be swept independently" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Inspecting Sweep Parameters\n", + "\n", + "You can list all available parameters for a pulse and retrieve their values." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available parameters for 'len_pulse':\n", + "['length', 'freq', 'gain', 'phase', 'total_length']\n", + "\n", + "Swept values:\n", + " length: [0.05045573 0.07486979 0.09928385 0.12369792 0.14811198]\n", + " total_length: [0.15136719 0.17578125 0.20019531 0.22460938 0.24902344]\n", + " gain: 1.0\n", + " phase: 0.0\n", + " freq: 499.99999923706054\n" + ] + } + ], + "source": [ + "# List all parameters for the pulse\n", + "print(\"Available parameters for 'len_pulse':\")\n", + "print(prog.list_pulse_params('len_pulse'))\n", + "\n", + "# Get all swept values as a dictionary\n", + "# This is useful for saving experiment metadata\n", + "swept_values = {}\n", + "for param in ['length', 'total_length', 'gain', 'phase', 'freq']:\n", + " try:\n", + " swept_values[param] = prog.get_pulse_param('len_pulse', param, as_array=True)\n", + " except:\n", + " pass # Parameter not swept or not available\n", + "\n", + "print(\"\\nSwept values:\")\n", + "for param, values in swept_values.items():\n", + " print(f\" {param}: {values}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Summary\n", + "\n", + "You have learned:\n", + "- How to create 1D sweeps using `add_loop()` and `QickSweep1D`\n", + "- How to create 2D sweeps using nested loops and `QickSpan`\n", + "- How to sweep pulse lengths and other parameters\n", + "- How to retrieve the actual swept values using `get_pulse_param()`\n", + "\n", + "**Key concepts:**\n", + "- Sweeps are executed entirely on the tProc, eliminating Python-FPGA latency\n", + "- Loop order determines the data shape (first declared = outermost)\n", + "- `QickSpan` allows building complex sweeps from simple components\n", + "- Always use `get_pulse_param()` to get the actual quantized values for accurate analysis\n", + "\n", + "**Next steps:** Proceed to [`03_Advanced_Timing.ipynb`](./03_Advanced_Timing.ipynb) to learn about timeline management, `delay` vs `wait`, and avoiding timing pitfalls." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/03_Advanced_Timing.ipynb b/docs/source/tutorials/03_Advanced_Timing.ipynb new file mode 100644 index 000000000..f878ba3f6 --- /dev/null +++ b/docs/source/tutorials/03_Advanced_Timing.ipynb @@ -0,0 +1,616 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 03 - Advanced Timing with tProc v2\n", + "\n", + "**Objective:** Understand the critical differences between `delay()`, `wait()`, `delay_auto()`, and `wait_auto()`. Learn how to manage the timeline correctly to avoid pulse collisions and timing errors." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding the Timeline\n", + "\n", + "In tProc v2, all timed instructions (pulses, triggers, delays) are scheduled on a common timeline. The tProc maintains a **reference time** that advances as instructions are executed.\n", + "\n", + "**Key concepts:**\n", + "- **Absolute time:** The actual time from the start of the shot\n", + "- **Reference time:** The tProc's internal clock, which determines when the next instruction will execute\n", + "- **Scheduled time:** The time you specify for a pulse or trigger (e.g., `t=0.5`)\n", + "\n", + "**The four timing commands:**\n", + "\n", + "| Command | What it does | When to use |\n", + "| :--- | :--- | :--- |\n", + "| `delay(t)` | Adds `t` to the reference time | When you know exactly how long to wait |\n", + "| `wait(t)` | Waits until the reference time reaches `t` | When you need to wait for a specific absolute time |\n", + "| `delay_auto(t, gens=True, ros=True)` | Adds `t` PLUS the time needed to complete all pending gens/ROs | Safe way to ensure everything finishes |\n", + "| `wait_auto(t, gens=False, ros=True)` | Waits until reference time reaches `t` OR pending operations finish | Similar to v1's `sync_all()` |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Basic Timing: `delay()` vs `wait()`\n", + "\n", + "The simplest timing commands are `delay()` and `wait()`. They work with absolute and relative timing." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING [asm_v2.py:998] warning: pulse time 0 appears to conflict with previous pulse ending at ?\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Using DELAY ===\n", + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&1] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " DPORT_WR p0 imm 1 @307 \n", + " TRIG p0 set @307 \n", + " DPORT_WR p0 imm 0 @317 \n", + " TRIG p0 clr @317 \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " TIME #492 inc_ref \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WAIT @1043 time \n", + " TIME #1350 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "\n", + "\n", + "=== Using WAIT ===\n", + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&1] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " DPORT_WR p0 imm 1 @307 \n", + " TRIG p0 set @307 \n", + " DPORT_WR p0 imm 0 @317 \n", + " TRIG p0 clr @317 \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WAIT @492 time \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WAIT @1535 time \n", + " TIME #1842 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "\n" + ] + } + ], + "source": [ + "class TimingExampleProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define a constant pulse\n", + " self.add_pulse(ch=gen_ch, name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['pulse_len'],\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # Trigger readout at t=0.5 µs\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.5)\n", + " \n", + " # First pulse at t=0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " \n", + " if cfg['use_wait']:\n", + " # Using wait: pause until reference time reaches 0.8 µs\n", + " self.wait(0.8)\n", + " else:\n", + " # Using delay: add 0.8 µs to reference time\n", + " self.delay(0.8)\n", + " \n", + " # Second pulse - when does it play?\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + "\n", + "# Compare delay vs wait\n", + "config = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'pulse_len': 0.1,\n", + " 'ro_len': 2.0,\n", + " 'use_wait': False\n", + "}\n", + "\n", + "print(\"=== Using DELAY ===\")\n", + "prog = TimingExampleProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "\n", + "# Get the compiled program as text to inspect the timing\n", + "print(prog.asm())\n", + "\n", + "print(\"\\n=== Using WAIT ===\")\n", + "config['use_wait'] = True\n", + "prog = TimingExampleProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "print(prog.asm())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Key difference:**\n", + "- `delay(0.8)` adds 0.8 µs to the reference time, so the second pulse plays 0.8 µs after the first pulse ends\n", + "- `wait(0.8)` pauses until the reference time reaches exactly 0.8 µs (absolute time from shot start)\n", + "\n", + "**Rule of thumb:** Use `delay()` for relative timing between pulses. Use `wait()` for absolute timing or when waiting for a specific absolute time." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Auto-Timing: `delay_auto()` and `wait_auto()`\n", + "\n", + "The `_auto` variants automatically account for pending operations (pulses that haven't finished, readouts that are still acquiring). This is safer when you're not sure how long previous operations will take." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&2] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " WPORT_WR p0 wmem [&0] @0 \n", + " TIME #184 inc_ref \n", + " WPORT_WR p0 wmem [&1] @0 \n", + " DPORT_WR p0 imm 1 @492 \n", + " TRIG p0 set @492 \n", + " DPORT_WR p0 imm 0 @502 \n", + " TRIG p0 clr @502 \n", + " WAIT @1414 time \n", + " TIME #1721 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "\n" + ] + } + ], + "source": [ + "class AutoTimingProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define pulses with swept lengths\n", + " self.add_pulse(ch=gen_ch, name=\"pulse_a\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['len_a'],\n", + " phase=0, gain=1.0)\n", + " \n", + " self.add_pulse(ch=gen_ch, name=\"pulse_b\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['len_b'],\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # Play first pulse at t=0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_a\", t=0)\n", + " \n", + " # Auto-delay: wait for pulse_a to finish, then add extra time\n", + " # ros=False means don't wait for readout (which hasn't started yet)\n", + " self.delay_auto(cfg['gap'], ros=False)\n", + " \n", + " # Play second pulse\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_b\", t=0)\n", + " \n", + " # Trigger readout after both pulses\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Demo: pulse lengths vary, but the gap between them stays constant\n", + "config_auto = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'len_a': 0.2, # µs\n", + " 'len_b': 0.3, # µs (different length!)\n", + " 'gap': 0.1, # µs gap between pulses\n", + " 'trig_time': 0.8, # µs\n", + " 'ro_len': 1.5 # µs\n", + "}\n", + "\n", + "prog = AutoTimingProgram(soccfg, reps=1, final_delay=0.5, cfg=config_auto)\n", + "\n", + "# Print the compiled program to see the generated assembly\n", + "print(prog.asm())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Why `delay_auto()` is powerful:**\n", + "- It automatically calculates when pulse A ends\n", + "- Then schedules pulse B to start at `(end_of_pulse_A + gap)`\n", + "- This works even if pulse lengths change (e.g., in a sweep)\n", + "\n", + "**Parameters:**\n", + "- `gens=True/False`: Whether to wait for generator operations (pulses)\n", + "- `ros=True/False`: Whether to wait for readout operations\n", + "- Default for `delay_auto()`: `gens=True, ros=True`\n", + "- Default for `wait_auto()`: `gens=False, ros=True` (like v1's `sync_all`)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Common Pitfall: Pulse Collisions\n", + "\n", + "If you schedule a pulse before the previous one finishes, the tProc will give you a warning (if you have logging enabled) but will still execute. This can cause unexpected results." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING [asm_v2.py:998] warning: pulse time 0.3 appears to conflict with previous pulse ending at ?\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Program with potential pulse collision:\n", + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&1] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WPORT_WR p0 wmem [&0] @184 \n", + " DPORT_WR p0 imm 1 @369 \n", + " TRIG p0 set @369 \n", + " DPORT_WR p0 imm 0 @379 \n", + " TRIG p0 clr @379 \n", + " WAIT @983 time \n", + " TIME #1290 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "\n", + "\n", + "Note: The second pulse is scheduled before the first finishes!\n" + ] + } + ], + "source": [ + "class CollisionProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"long_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], length=0.5, phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # First pulse at t=0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"long_pulse\", t=0)\n", + " \n", + " # This pulse is scheduled while the first is still playing!\n", + " # The tProc will issue a warning.\n", + " self.pulse(ch=cfg['gen_ch'], name=\"long_pulse\", t=0.3)\n", + " \n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.6)\n", + "\n", + "config_collision = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'ro_len': 1.0\n", + "}\n", + "\n", + "prog = CollisionProgram(soccfg, reps=1, final_delay=0.5, cfg=config_collision)\n", + "print(\"Program with potential pulse collision:\")\n", + "print(prog.asm())\n", + "print(\"\\nNote: The second pulse is scheduled before the first finishes!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**How to fix:** Use `delay_auto()` to ensure proper spacing.\n", + "\n", + "```python\n", + "# Instead of:\n", + "self.pulse(ch=gen_ch, name=\"long_pulse\", t=0)\n", + "self.pulse(ch=gen_ch, name=\"long_pulse\", t=0.3) # Collision!\n", + "\n", + "# Do this:\n", + "self.pulse(ch=gen_ch, name=\"long_pulse\", t=0)\n", + "self.delay_auto(0.1) # Wait for pulse to finish, then add 0.1 µs\n", + "self.pulse(ch=gen_ch, name=\"long_pulse\", t=0) # Safely scheduled\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Sweeping Delays\n", + "\n", + "You can sweep delay times just like pulse parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Program with swept delay:\n", + " NOP \n", + " REG_WR r2 imm #61 \n", + " REG_WR r3 imm #1167 \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&1] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " REG_WR r1 imm #0 \n", + "delay_sweep:\n", + " DPORT_WR p0 imm 1 @307 \n", + " TRIG p0 set @307 \n", + " DPORT_WR p0 imm 0 @317 \n", + " TRIG p0 clr @317 \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " TIME inc_ref r2 \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WAIT @860 time \n", + " TIME inc_ref r3 \n", + " REG_WR s12 op -op(s12 + #1) \n", + " REG_WR r2 op -op(r2 + #61) \n", + " REG_WR r3 op -op(r3 + #-61) \n", + " TEST -op(r1 - #4) \n", + " JUMP delay_sweep -if(NZ) -wr(r1 op) -op(r1 + #1) \n", + " REG_WR r2 op -op(r2 + #-305) \n", + " REG_WR r3 op -op(r3 + #305) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "\n", + "\n", + "Note: The tProc will automatically handle the varying delay times.\n" + ] + } + ], + "source": [ + "class DelaySweepProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " self.add_loop(\"delay_sweep\", self.cfg[\"steps\"])\n", + "\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], length=0.1,\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # Trigger readout at fixed time\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.5)\n", + " \n", + " # First pulse at t=0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " \n", + " # Sweep the delay between pulses\n", + " self.delay(QickSweep1D(\"delay_sweep\", 0.1, 0.5))\n", + " \n", + " # Second pulse\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + "\n", + "config_delay_sweep = {\n", + " 'steps': 5,\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'ro_len': 1.0\n", + "}\n", + "\n", + "prog = DelaySweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config_delay_sweep)\n", + "\n", + "# Show that the program compiles with swept delays\n", + "print(\"Program with swept delay:\")\n", + "print(prog.asm())\n", + "print(\"\\nNote: The tProc will automatically handle the varying delay times.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Summary\n", + "\n", + "You have learned:\n", + "- The difference between `delay()` (relative) and `wait()` (absolute)\n", + "- How `delay_auto()` and `wait_auto()` automatically account for pending operations\n", + "- How to avoid pulse collisions by using proper timing commands\n", + "- How to sweep delay times\n", + "- How to inspect the generated assembly code using `asm()` to verify timing\n", + "\n", + "**Best practices:**\n", + "1. **Use `delay_auto()` when you want to wait for pulses to finish** - It's safer and works with swept lengths\n", + "2. **Use `wait()` for absolute timing** - When you need to hit an exact absolute time\n", + "3. **Use `delay()` for simple relative timing** - When you know exactly how long to wait\n", + "4. **Always check for pulse collisions** - Use `print(prog.asm())` to inspect the generated code\n", + "5. **Use tags to identify instructions** - Tags help with debugging and retrieving parameters\n", + "\n", + "**Next steps:** Proceed to [`04_Real_Time_Feedback.ipynb`](./04_Real_Time_Feedback.ipynb) to learn how to read measurement results and make decisions on the tProc in real-time." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/04_Real_Time_Feedback.ipynb b/docs/source/tutorials/04_Real_Time_Feedback.ipynb new file mode 100644 index 000000000..5c8781d00 --- /dev/null +++ b/docs/source/tutorials/04_Real_Time_Feedback.ipynb @@ -0,0 +1,608 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 04 - Real-Time Feedback with tProc v2\n", + "\n", + "**Objective:** Learn how to read measurement results directly on the tProc and make real-time decisions (conditional pulses, active reset, adaptive measurements)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding tProc Feedback\n", + "\n", + "The tProc can read accumulated IQ values from the readout module and use them to make decisions in real-time. This enables:\n", + "- Conditional pulse sequences (e.g., play a second pulse only if a measurement exceeds a threshold)\n", + "- Active reset (play a pulse conditioned on measurement outcome)\n", + "- Adaptive measurements (change experiment parameters based on results)\n", + "\n", + "**Key commands:**\n", + "- `read_input(ro_ch)` - Reads the accumulated IQ value from a readout channel into registers `s_port_l` (I) and `s_port_h` (Q)\n", + "- `cond_jump(label, condition)` - Conditional jump based on register values\n", + "- `read_and_jump()` - Convenience method that combines both\n", + "\n", + "**Important:** After triggering a readout, you must wait for it to complete before reading the value. This requires tuning the `read_wait` parameter." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Measuring Readout Latency\n", + "\n", + "Before using feedback, you need to determine the correct `read_wait` time. This is the delay between triggering a readout and when the accumulated value is available to the tProc." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Buffered I/Q: (-0, 0)\n", + "Feedback I/Q: (12, 5)\n", + "\n", + "✗ Feedback and buffered readouts do NOT match! Increase 'read_wait'.\n" + ] + } + ], + "source": [ + "class LatencyMeasurementProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define a constant test pulse\n", + " self.add_pulse(ch=gen_ch, name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['pulse_len'],\n", + " phase=cfg['pulse_phase'],\n", + " gain=cfg['gain'])\n", + "\n", + " # Configure readout\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch, \n", + " phase=cfg['ro_phase'])\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # Play pulse\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " \n", + " # Trigger readout\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + " \n", + " # Wait for readout to complete\n", + " self.wait_auto(cfg['read_wait'])\n", + " \n", + " # Read the accumulated I/Q value into tProc registers\n", + " self.read_input(ro_ch=cfg['ro_ch'])\n", + " \n", + " # Store the values to data memory for reading back from Python\n", + " self.write_dmem(addr=0, src='s_port_l') # I value (lower 32 bits)\n", + " self.write_dmem(addr=1, src='s_port_h') # Q value (upper 32 bits)\n", + "\n", + "# Configuration\n", + "config_latency = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'pulse_len': 0.025,\n", + " 'pulse_phase': 0,\n", + " 'ro_phase': 0,\n", + " 'gain': 1.0,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.1,\n", + " 'read_wait': 0.1 # This needs to be tuned\n", + "}\n", + "\n", + "# Run the measurement\n", + "prog = LatencyMeasurementProgram(soccfg, reps=100, final_delay=0.5, cfg=config_latency)\n", + "\n", + "# Acquire data - this also gives us the buffered readout values\n", + "iq_data = prog.acquire(soc, rounds=1, progress=False)\n", + "\n", + "# Get the feedback readout values from data memory\n", + "# read_mem returns a numpy array of int32 values\n", + "I_feedback = soc.read_mem(length=1, mem_sel='dmem', addr=0)[0]\n", + "Q_feedback = soc.read_mem(length=1, mem_sel='dmem', addr=1)[0]\n", + "\n", + "# Get the buffered readout values (from acquire())\n", + "# Shape is (1, 2) → (rounds, IQ)\n", + "I_buffered = iq_data[0][0, 0] * prog.cfg['ro_len'] # round 0, I\n", + "Q_buffered = iq_data[0][0, 1] * prog.cfg['ro_len'] # round 0, Q\n", + "\n", + "print(f\"Buffered I/Q: ({I_buffered:.0f}, {Q_buffered:.0f})\")\n", + "print(f\"Feedback I/Q: ({I_feedback}, {Q_feedback})\")\n", + "\n", + "if I_feedback == I_buffered and Q_feedback == Q_buffered:\n", + " print(\"\\n✓ Feedback and buffered readouts match! Latency is correct.\")\n", + "else:\n", + " print(\"\\n✗ Feedback and buffered readouts do NOT match! Increase 'read_wait'.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- The buffered readout (from `acquire()`) is reliable and length-normalized\n", + "- The feedback readout (via `read_input()`) gives raw accumulated values\n", + "- If they don't match, increase `read_wait` until they do\n", + "- Once tuned, this `read_wait` value can be used for all feedback experiments" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Conditional Pulse: If-Then-Else on the tProc\n", + "\n", + "Now let's use feedback to conditionally play a second pulse based on the measurement result." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f8ea227444e4350bff98d3ec996e48b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class ConditionalPulseProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " self.add_loop(\"feedback_sweep\", self.cfg[\"steps\"])\n", + "\n", + " # Define pulses\n", + " self.add_pulse(ch=gen_ch, name=\"cond_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['pulse_len'],\n", + " phase=cfg['pulse_phase'],\n", + " gain=cfg['gain'])\n", + "\n", + " # Configure readout with phase rotation for optimal sensitivity\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch, \n", + " phase=cfg['ro_phase'])\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # First pulse and readout (the measurement)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"cond_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + " # Wait for readout to complete (tuned latency)\n", + " self.wait_auto(cfg['read_wait'])\n", + " self.delay_auto(cfg['read_wait'] + cfg['extra_delay'])\n", + "\n", + " # Second readout (for feedback decision)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + " # If I >= threshold, jump to 'SKIP_PULSE'\n", + " self.read_and_jump(ro_ch=cfg['ro_ch'], \n", + " component='I', \n", + " threshold=cfg['threshold'], \n", + " test='>=', \n", + " label='SKIP_PULSE')\n", + " \n", + " # --- This code is skipped if condition is true ---\n", + " # Second pulse (only played if I < threshold)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"cond_pulse\", t=0, tag='conditional_pulse')\n", + " \n", + " # Label for conditional jump\n", + " self.label(\"SKIP_PULSE\")\n", + "\n", + "# Configuration\n", + "config_conditional = {\n", + " 'steps': 9,\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'read_wait': 0.1, # Tuned from latency measurement\n", + " 'extra_delay': 0.2, # Extra safety margin\n", + " 'threshold': 0, # Threshold on I component\n", + " 'ro_len': 0.1,\n", + " 'pulse_len': 0.025,\n", + " 'pulse_phase': 0,\n", + " 'ro_phase': 0, # Optimize via rotation\n", + " 'gain': QickSweep1D(\"feedback_sweep\", -1.0, 1.0)\n", + "}\n", + "\n", + "# Run the experiment\n", + "prog = ConditionalPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config_conditional)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot results for each sweep step\n", + "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + "for i_read in range(2):\n", + " ax = axes[i_read]\n", + " for i_step, iq_point in enumerate(iq_data[0]):\n", + " ax.plot(time_axis, iq_point[i_read, :, 0], label=f\"Step {i_step}\")\n", + " ax.set_title(f\"Readout {i_read+1} - I Component\")\n", + " ax.set_xlabel(\"Time (µs)\")\n", + " ax.set_ylabel(\"Amplitude (a.u.)\")\n", + " ax.legend()\n", + " ax.grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- The first readout measures the result\n", + "- If `I >= threshold`, the tProc jumps to `SKIP_PULSE`, skipping the second pulse\n", + "- If `I < threshold`, the second pulse is played\n", + "- This runs entirely on the tProc with no Python involvement\n", + "\n", + "**Note:** The threshold comparison uses raw (non-normalized) accumulated values. The same `read_wait` tuned from the latency measurement is critical here." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Active Reset\n", + "\n", + "A common feedback application is active reset: play a reset pulse conditioned on the qubit state." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Active reset program created successfully.\n", + "In practice, you would add a verification readout after the reset pulse.\n" + ] + } + ], + "source": [ + "class ActiveResetProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Measurement pulse\n", + " self.add_pulse(ch=gen_ch, name=\"measure_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['pulse_len'],\n", + " phase=0, gain=1.0)\n", + " \n", + " # Reset pulse (π pulse to bring qubit to ground)\n", + " self.add_pulse(ch=gen_ch, name=\"reset_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], \n", + " length=cfg['reset_len'],\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch, \n", + " phase=cfg['ro_phase'])\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " # Measurement\n", + " self.pulse(ch=cfg['gen_ch'], name=\"measure_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + " self.wait_auto(cfg['read_wait'])\n", + " self.delay_auto(cfg['read_wait'] + cfg['extra_delay'])\n", + "\n", + " # If I >= threshold (qubit in excited state), play reset pulse\n", + " self.read_and_jump(ro_ch=cfg['ro_ch'], \n", + " component='I', \n", + " threshold=cfg['threshold'], \n", + " test='>=', \n", + " label='NO_RESET')\n", + " \n", + " self.pulse(ch=cfg['gen_ch'], name=\"reset_pulse\", t=0, tag='reset_pulse')\n", + " self.label(\"NO_RESET\")\n", + "\n", + "config_reset = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'read_wait': 0.1,\n", + " 'extra_delay': 0.2,\n", + " 'threshold': 0,\n", + " 'ro_len': 0.1,\n", + " 'pulse_len': 0.025,\n", + " 'reset_len': 0.05,\n", + " 'ro_phase': 0\n", + "}\n", + "\n", + "prog = ActiveResetProgram(soccfg, reps=100, final_delay=0.5, cfg=config_reset)\n", + "\n", + "# In a real experiment, you would verify that the reset works by\n", + "# measuring the state after reset. This would require a second readout.\n", + "print(\"Active reset program created successfully.\")\n", + "print(\"In practice, you would add a verification readout after the reset pulse.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Visualizing the Feedback Decision\n", + "\n", + "Let's examine how the threshold affects the conditional pulse decision." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e65b9f1aa9834fa791658680ef6ea8a3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/2000 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Conditional pulse played for 1003 out of 2000 steps\n", + "Trigger threshold crossed when I_first >= 2000\n" + ] + } + ], + "source": [ + "# Run a sweep with varying gain and observe when the conditional pulse triggers\n", + "config_conditional['steps'] = 2000\n", + "config_conditional['threshold'] = 2000 # Set a fixed threshold\n", + "\n", + "prog = ConditionalPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config_conditional)\n", + "\n", + "# Acquire data without decimation to get single-shot values\n", + "iq_data = prog.acquire(soc, rounds=1)\n", + "gains = prog.get_pulse_param('cond_pulse', 'gain', as_array=True)\n", + "\n", + "print(\"iq_data length:\", len(iq_data))\n", + "for i, d in enumerate(iq_data):\n", + " print(f\"iq_data[{i}] shape:\", d.shape)\n", + "# The shape: (reps, steps, readout_length, I/Q)\n", + "# For feedback, we care about the integrated value (sum over readout window)\n", + "# Shape: (readouts, steps, IQ)\n", + "# iq_data[0][readout, step, I/Q]\n", + "\n", + "I_first = iq_data[0][0, :, 0] * prog.cfg['ro_len'] # primer readout, I, todos los steps\n", + "I_second = iq_data[0][1, :, 0] * prog.cfg['ro_len'] # segundo readout, I, todos los steps\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(gains, I_first, 'o-', label='First readout (measurement)')\n", + "plt.plot(gains, I_second, 's-', label='Second readout (conditional)')\n", + "plt.axhline(y=config_conditional['threshold'], color='r', linestyle='--', \n", + " label=f'Threshold = {config_conditional[\"threshold\"]}')\n", + "plt.xlabel('Gain')\n", + "plt.ylabel('Integrated I (a.u.)')\n", + "plt.title('Feedback Decision: Conditional Pulse Trigger')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "triggered = I_second > 0\n", + "print(f\"Conditional pulse played for {np.sum(triggered)} out of {len(gains)} steps\")\n", + "print(f\"Trigger threshold crossed when I_first >= {config_conditional['threshold']}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Observations:**\n", + "- The first readout (measurement) varies with gain as expected\n", + "- The second readout (conditional) is zero when the condition fails (pulse not played)\n", + "- The second readout has a signal only when the conditional pulse was actually played\n", + "- The threshold determines the decision boundary" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Summary\n", + "\n", + "You have learned:\n", + "- How to measure and tune readout latency for reliable feedback\n", + "- How to read accumulated IQ values directly on the tProc using `read_input()`\n", + "- How to implement conditional pulses using `read_and_jump()`\n", + "- How to implement active reset for qubit state initialization\n", + "- How to verify that your feedback logic is working correctly\n", + "\n", + "**Key concepts:**\n", + "- `read_wait` must be tuned to match your hardware latency\n", + "- Feedback uses raw (non-normalized) accumulated values\n", + "- The tProc can make decisions in microseconds, enabling active reset and adaptive protocols\n", + "- Always verify feedback behavior with benchmark measurements\n", + "- Use `read_mem()` to retrieve values written to data memory\n", + "\n", + "**Next steps:** Proceed to [`05_Dynamic_Parameters_Subroutines.ipynb`](./05_Dynamic_Parameters_Subroutines.ipynb) to learn how to modify pulse parameters dynamically and use subroutines for code reuse." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/05_Dynamic_Parameters_Subroutines.ipynb b/docs/source/tutorials/05_Dynamic_Parameters_Subroutines.ipynb new file mode 100644 index 000000000..43d718ee0 --- /dev/null +++ b/docs/source/tutorials/05_Dynamic_Parameters_Subroutines.ipynb @@ -0,0 +1,720 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 05 - Dynamic Parameters and Subroutines with tProc v2\n", + "\n", + "**Objective:** Learn how to modify pulse parameters dynamically in real-time (virtual-Z gates, phase updates) and use subroutines for efficient code reuse on the tProc." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, AsmV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding Dynamic Parameter Updates\n", + "\n", + "In tProc v2, pulse parameters are stored in **waveform memory**. Each pulse consists of one or more waveforms (for flat-top pulses: rise, flat, fall). You can:\n", + "- **Read** a waveform from memory into waveform registers\n", + "- **Modify** parameters in the registers (frequency, phase, gain, length)\n", + "- **Write** the modified waveform back to memory\n", + "\n", + "This allows you to change pulse properties on-the-fly without recompiling the program.\n", + "\n", + "**Key operations:**\n", + "- `read_wmem(name)` - Read a waveform into registers\n", + "- `write_wmem(name)` - Write registers back to waveform memory\n", + "- Waveform registers: `w_freq`, `w_phase`, `w_gain`, `w_length`, `w_env`, `w_conf`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Virtual-Z Gate: Updating Phase Dynamically\n", + "\n", + "A virtual-Z gate is a phase update applied to subsequent pulses without playing an actual pulse. This is essential for many quantum control sequences." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "d05e3cb4906c48ea8659581ea582fd54", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "=== Virtual-Z Subroutine ASM ===\n", + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&3] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " DPORT_WR p0 imm 1 @154 \n", + " TRIG p0 set @154 \n", + " TRIG p7 set @154 \n", + " DPORT_WR p0 imm 0 @164 \n", + " TRIG p0 clr @164 \n", + " TRIG p7 clr @164 \n", + " WPORT_WR p0 wmem [&0] @0 \n", + " WPORT_WR p0 wmem [&1] @0 \n", + " WPORT_WR p0 wmem [&2] @0 \n", + " CALL virtual_z \n", + " WPORT_WR p0 wmem [&0] @123 \n", + " WPORT_WR p0 wmem [&1] @123 \n", + " WPORT_WR p0 wmem [&2] @123 \n", + " WPORT_WR p0 wmem [&0] @246 \n", + " WPORT_WR p0 wmem [&1] @246 \n", + " WPORT_WR p0 wmem [&2] @246 \n", + " CALL virtual_z \n", + " WPORT_WR p0 wmem [&0] @369 \n", + " WPORT_WR p0 wmem [&1] @369 \n", + " WPORT_WR p0 wmem [&2] @369 \n", + " REG_WR r_wave wmem [&0] \n", + " REG_WR w1 imm #0 \n", + " WMEM_WR &0 \n", + " REG_WR r_wave wmem [&1] \n", + " REG_WR w1 imm #0 \n", + " WMEM_WR &1 \n", + " REG_WR r_wave wmem [&2] \n", + " REG_WR w1 imm #0 \n", + " WMEM_WR &2 \n", + " WAIT @706 time \n", + " TIME #1013 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #1) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "virtual_z:\n", + " REG_WR r_wave wmem [&0] \n", + " REG_WR r1 imm #1073741824 \n", + " REG_WR w1 op -op(w1 + r1) \n", + " WMEM_WR &0 \n", + " REG_WR r_wave wmem [&1] \n", + " REG_WR r1 imm #1073741824 \n", + " REG_WR w1 op -op(w1 + r1) \n", + " WMEM_WR &1 \n", + " REG_WR r_wave wmem [&2] \n", + " REG_WR r1 imm #1073741824 \n", + " REG_WR w1 op -op(w1 + r1) \n", + " WMEM_WR &2 \n", + "RET\n", + "\n", + "\n" + ] + } + ], + "source": [ + "class VirtualZProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define a Gaussian envelope for pulse shaping\n", + " ramp_len = 0.1\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", \n", + " sigma=ramp_len/10, \n", + " length=ramp_len, \n", + " even_length=True)\n", + " \n", + " # Define a flat-top pulse (has 3 waveforms: rise, flat, fall)\n", + " self.add_pulse(ch=gen_ch, name=\"my_pulse\", \n", + " style=\"flat_top\", \n", + " envelope=\"ramp\", \n", + " freq=cfg['freq'], \n", + " length=0.05,\n", + " phase=0,\n", + " gain=1.0)\n", + "\n", + " # Create a subroutine for virtual-Z (90-degree phase rotation)\n", + " sub = AsmV2()\n", + " for wname in self.list_pulse_waveforms(\"my_pulse\"):\n", + " sub.read_wmem(name=wname)\n", + " sub.inc_reg(dst='w_phase', src=self.deg2reg(90, gen_ch=gen_ch))\n", + " sub.write_wmem(name=wname)\n", + " self.add_subroutine(\"virtual_z\", sub)\n", + "\n", + " # Configure readout\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + "\n", + " # Pulse 1: phase = 0\n", + " self.pulse(ch=cfg['gen_ch'], name=\"my_pulse\", t=0, tag='pulse1')\n", + " \n", + " # Apply virtual-Z (adds 90° to subsequent pulses)\n", + " self.call(\"virtual_z\")\n", + " \n", + " # Pulse 2: phase = 90°\n", + " self.pulse(ch=cfg['gen_ch'], name=\"my_pulse\", t=0.2, tag='pulse2')\n", + " \n", + " # Pulse 3: phase = 90° (same as pulse 2)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"my_pulse\", t=0.4, tag='pulse3')\n", + " \n", + " # Apply another virtual-Z (adds another 90° → total 180°)\n", + " self.call(\"virtual_z\")\n", + " \n", + " # Pulse 4: phase = 180°\n", + " self.pulse(ch=cfg['gen_ch'], name=\"my_pulse\", t=0.6, tag='pulse4')\n", + " \n", + " # Reset phase to 0 for next shot\n", + " for wname in self.list_pulse_waveforms(\"my_pulse\"):\n", + " self.read_wmem(name=wname)\n", + " self.write_reg(dst='w_phase', src=0)\n", + " self.write_wmem(name=wname)\n", + "\n", + "# Configuration\n", + "config_vz = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.9\n", + "}\n", + "\n", + "# Run the program\n", + "prog = VirtualZProgram(soccfg, reps=2, final_delay=0.5, cfg=config_vz)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot results for each repetition\n", + "fig, axes = plt.subplots(len(iq_data[0]), 1, figsize=(10, 6))\n", + "if len(iq_data[0]) == 1:\n", + " axes = [axes]\n", + "\n", + "for rep, iq in enumerate(iq_data[0]):\n", + " ax = axes[rep]\n", + " ax.plot(time_axis, iq[:,0], label='I')\n", + " ax.plot(time_axis, iq[:,1], label='Q')\n", + " ax.plot(time_axis, np.abs(iq.dot([1,1j])), label='Magnitude')\n", + " ax.legend()\n", + " ax.set_title(f\"Repetition {rep}\")\n", + " ax.set_ylabel(\"Amplitude (a.u.)\")\n", + " ax.set_xlabel(\"Time (µs)\")\n", + " ax.grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Inspect the subroutine in the compiled program\n", + "print(\"\\n=== Virtual-Z Subroutine ASM ===\")\n", + "print(prog.asm())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- The virtual-Z subroutine reads each waveform, adds 90° to its phase, and writes it back\n", + "- `deg2reg()` converts degrees to the tProc's internal phase units\n", + "- After the sequence, we reset the phase to 0 for the next shot\n", + "- Subroutines are defined using `AsmV2()` and added with `add_subroutine()`\n", + "- Called with `call(\"name\")`\n", + "- Use `asm_subroutine()` to see the generated assembly for a subroutine" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Understanding Waveform Structure\n", + "\n", + "Different pulse styles have different waveform counts:\n", + "- `const` - 1 waveform\n", + "- `arb` (arbitrary/envelope) - 1 waveform\n", + "- `flat_top` - 3 waveforms (rise, flat, fall)\n", + "\n", + "You can list waveforms for any pulse:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Waveforms for 'my_pulse':\n", + " my_pulse_w0\n", + " my_pulse_w1\n", + " my_pulse_w2\n", + "\n", + "First waveform parameters: {'freq': QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), 'phase': QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), 'env': 0, 'gain': QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), 'length': 31, 'conf': 8, 'name': 'my_pulse_w0'}\n" + ] + } + ], + "source": [ + "print(\"Waveforms for 'my_pulse':\")\n", + "for wname in prog.list_pulse_waveforms(\"my_pulse\"):\n", + " print(f\" {wname}\")\n", + "\n", + "# You can also inspect parameters of individual waveforms\n", + "waveform = prog.pulses['my_pulse'].waveforms[0]\n", + "print(f\"\\nFirst waveform parameters: {waveform.__dict__}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Subroutines for Code Reuse\n", + "\n", + "Subroutines are useful for code that repeats across multiple places in your program. They can be called multiple times." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Subroutine macro_list ===\n", + "Pulse(ch=0, name='test_pulse', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "Delay(t=0.15, auto=False, tag=None, t_params={'t': }, t_regs={'t': 92})\n", + "Pulse(ch=0, name='test_pulse', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\n", + "=== Fully compiled program ===\n", + " NOP \n", + " REG_WR s12 imm #0 \n", + " WPORT_WR p3 wmem [&1] @0 \n", + " TIME #614 inc_ref \n", + " REG_WR r0 imm #0 \n", + "reps:\n", + " DPORT_WR p0 imm 1 @154 \n", + " TRIG p0 set @154 \n", + " DPORT_WR p0 imm 0 @164 \n", + " TRIG p0 clr @164 \n", + " CALL pulse_pair \n", + " TIME #123 inc_ref \n", + " CALL pulse_pair \n", + " WAIT @953 time \n", + " TIME #1260 inc_ref \n", + " REG_WR s12 op -op(s12 + #1) \n", + " TEST -op(r0 - #0) \n", + " JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + " JUMP HERE \n", + "pulse_pair:\n", + " WPORT_WR p0 wmem [&0] @0 \n", + " TIME #92 inc_ref \n", + " WPORT_WR p0 wmem [&0] @0 \n", + "RET\n", + "\n", + "\n" + ] + } + ], + "source": [ + "class SubroutineExampleProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define a simple pulse\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], length=0.1,\n", + " phase=0, gain=1.0)\n", + "\n", + " # Define a subroutine that plays a pulse sequence\n", + " sub = AsmV2()\n", + " sub.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " sub.delay(0.15)\n", + " sub.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " self.add_subroutine(\"pulse_pair\", sub)\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + " \n", + " # Call the subroutine multiple times\n", + " self.call(\"pulse_pair\") # First pair\n", + " self.delay(0.2)\n", + " self.call(\"pulse_pair\") # Second pair\n", + "\n", + "config_sub = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 1.5\n", + "}\n", + "\n", + "prog = SubroutineExampleProgram(soccfg, reps=1, final_delay=0.5, cfg=config_sub)\n", + "\n", + "print(\"=== Subroutine macro_list ===\")\n", + "for inst in prog.subroutines['pulse_pair'].macro_list:\n", + " print(inst)\n", + "\n", + "print(\"\\n=== Fully compiled program ===\")\n", + "print(prog.asm())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Benefits of subroutines:**\n", + "- Reduce program size (important since program memory is limited to 4096 words)\n", + "- Make code more readable and maintainable\n", + "- Allow complex sequences to be built from reusable components\n", + "\n", + "**Limitation:** The timeline management code doesn't understand jumps. If your subroutine contains timed instructions, ensure the timeline is consistent before and after the call. For simple pulse sequences without timeline dependencies, subroutines work well." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Dynamic Gain Modulation\n", + "\n", + "You can also update gain dynamically to implement amplitude modulation." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "728f48b71daf4c6db5d8457e60467735", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class GainModulationProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " \n", + " # Define a Gaussian envelope\n", + " ramp_len = 0.1\n", + " self.add_gauss(ch=cfg['gen_ch'], name=\"ramp\", \n", + " sigma=ramp_len/10, length=ramp_len, even_length=True)\n", + " \n", + " # Define three pulses with different gains\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse_full\", \n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=0.05, phase=0, gain=1.0)\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse_half\", \n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=0.05, phase=0, gain=0.5)\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse_quarter\", \n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=0.05, phase=0, gain=0.25)\n", + " \n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " \n", + " # Pulse with full gain\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_full\", t=0, tag='pulse_full')\n", + " \n", + " # Pulse with half gain\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_half\", t=0.2, tag='pulse_half')\n", + " \n", + " # Pulse with quarter gain\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_quarter\", t=0.4, tag='pulse_quarter')\n", + "\n", + "\n", + "prog = GainModulationProgram(soccfg, reps=1, final_delay=0.5, cfg=config_sub)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q')\n", + "plt.plot(time_axis, np.abs(iq_data[0].dot([1,1j])), label='Magnitude')\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('Dynamic Gain Modulation')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Note on gain operations:**\n", + "- Gain is stored as a 16-bit integer (0-32766 for full scale)\n", + "- Shifting right (>>) divides by 2\n", + "- Shifting left (<<) multiplies by 2\n", + "- Be careful not to exceed the maximum value (32766)\n", + "- Use `reg_op()` for arithmetic operations on registers" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Inspecting Waveform Memory\n", + "\n", + "You can inspect the contents of waveform memory for debugging." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All defined pulses:\n", + "\n", + "Pulse: pulse_full\n", + " type: \n", + " waveforms: [Waveform(name='pulse_full_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=31, conf=8), Waveform(name='pulse_full_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9), Waveform(name='pulse_full_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=31, conf=8)]\n", + " waveform: Waveform(name='pulse_full_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_full_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16383, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_full_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + "\n", + "Pulse: pulse_half\n", + " type: \n", + " waveforms: [Waveform(name='pulse_half_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16382, spans={}, quantize=2, steps={}), length=31, conf=8), Waveform(name='pulse_half_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=8191, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9), Waveform(name='pulse_half_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=16382, spans={}, quantize=2, steps={}), length=31, conf=8)]\n", + " waveform: Waveform(name='pulse_half_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=16382, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_half_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=8191, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_half_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=16382, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + "\n", + "Pulse: pulse_quarter\n", + " type: \n", + " waveforms: [Waveform(name='pulse_quarter_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=8190, spans={}, quantize=2, steps={}), length=31, conf=8), Waveform(name='pulse_quarter_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=4095, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9), Waveform(name='pulse_quarter_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=8190, spans={}, quantize=2, steps={}), length=31, conf=8)]\n", + " waveform: Waveform(name='pulse_quarter_w0', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=8190, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_quarter_w1', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=4095, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=31, spans={}, quantize=1, steps={}), conf=9)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + " waveform: Waveform(name='pulse_quarter_w2', freq=QickRawParam(par='freq', start=218453333, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=31, gain=QickRawParam(par='gain', start=8190, spans={}, quantize=2, steps={}), length=31, conf=8)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n", + "\n", + "Pulse: my_ro\n", + " type: \n", + " waveforms: [Waveform(name='my_ro_w0', freq=QickRawParam(par='freq', start=-873813332, spans={}, quantize=-4, steps={}), phase=0, env=0, gain=0, length=3, conf=520)]\n", + " waveform: Waveform(name='my_ro_w0', freq=QickRawParam(par='freq', start=-873813332, spans={}, quantize=-4, steps={}), phase=0, env=0, gain=0, length=3, conf=520)\n", + " attributes: ['compile', 'conf', 'env', 'fill_steps', 'freq', 'gain', 'get', 'items', 'keys', 'length', 'name', 'phase', 'sweeps', 'to_dict', 'values', 'widths']\n" + ] + } + ], + "source": [ + "# Print all defined pulses after program execution\n", + "print(\"All defined pulses:\")\n", + "for pulse_name, pulse_obj in prog.pulses.items():\n", + " print(f\"\\nPulse: {pulse_name}\")\n", + " print(f\" type: {type(pulse_obj)}\")\n", + " print(f\" waveforms: {pulse_obj.waveforms}\")\n", + " for waveform in pulse_obj.waveforms:\n", + " print(f\" waveform: {waveform}\")\n", + " print(f\" attributes: {[a for a in dir(waveform) if not a.startswith('_')]}\")\n", + "\n", + "# You can also read waveform memory from the board\n", + "# This requires knowing the waveform addresses\n", + "# waddr = prog.pulse_waveform_addrs['my_pulse']['my_pulse_w0']\n", + "# waveform_data = soc.read_mem(length=100, mem_sel='wmem', addr=waddr)\n", + "# Note: read_mem returns data as 32-bit integers; you may need to reinterpret as 16-bit I/Q pairs" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "You have learned:\n", + "- How to read, modify, and write waveforms dynamically using waveform registers\n", + "- How to implement virtual-Z gates by updating pulse phase\n", + "- How to create and use subroutines for code reuse\n", + "- How to modulate gain dynamically\n", + "- How to inspect waveform parameters\n", + "- How to use `read_mem()` to read waveform memory for debugging\n", + "\n", + "**Key concepts:**\n", + "- Waveform memory stores pulse definitions that can be modified at runtime\n", + "- Subroutines save program memory and improve code organization\n", + "- The tProc can update waveform parameters in microseconds, enabling adaptive pulse shaping\n", + "- Always reset modified parameters between shots unless persistence is intended\n", + "\n", + "**Next steps:** Proceed to [`06_Generators_And_Readouts.ipynb`](./06_Generators_And_Readouts.ipynb) to learn about the different generators and readout in QICK." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/06_Generators_And_Readouts.ipynb b/docs/source/tutorials/06_Generators_And_Readouts.ipynb new file mode 100644 index 000000000..67002c146 --- /dev/null +++ b/docs/source/tutorials/06_Generators_And_Readouts.ipynb @@ -0,0 +1,1594 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 06 - Generators and Readouts\n", + "\n", + "**Objective:** Learn how to use waveform generators and optimize readout resonators. This notebook covers:\n", + "- Generator configuration and waveform styles\n", + "- Readout resonator tuning and optimization\n", + "- IQ mixing and signal processing\n", + "- Frequency sweeps for resonator characterization" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0 # Generator channel (DAC)\n", + "RO_CH = 0 # Readout channel (ADC)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding Waveform Generators\n", + "\n", + "The QICK board has multiple DAC channels that can generate arbitrary waveforms. Each generator can produce:\n", + "- **Constant pulses** (`style=\"const\"`): Square pulses with constant amplitude\n", + "- **Arbitrary/envelope pulses** (`style=\"arb\"`): Shaped pulses using a predefined envelope\n", + "- **Flat-top pulses** (`style=\"flat_top\"`): Gaussian rise/fall with constant middle section\n", + "\n", + "Let's explore each type." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Generator Configuration: Constant Pulse\n", + "\n", + "The simplest waveform is a constant pulse. It has a flat amplitude over its duration." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ef20eb1bd91e4243b64850aa80326d4a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class ConstantPulseProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Constant pulse - square shape\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=\"const_pulse\",\n", + " style=\"const\",\n", + " freq=cfg['freq'],\n", + " length=cfg['pulse_len'],\n", + " phase=cfg['phase'],\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + " self.pulse(ch=cfg['gen_ch'], name=\"const_pulse\", t=0)\n", + "\n", + "# Configure and run\n", + "config_const = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 100.0,\n", + " 'pulse_len': 0.2,\n", + " 'phase': 0,\n", + " 'gain': 0.8,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.5\n", + "}\n", + "\n", + "prog = ConstantPulseProgram(soccfg, reps=1, final_delay=1.0, cfg=config_const)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "plt.figure(figsize=(10, 4))\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q')\n", + "plt.legend()\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('Constant Pulse - I/Q Components')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Observation:** The constant pulse produces a square-shaped response. The I and Q components oscillate at the carrier frequency (100 MHz in this case)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Shaped Pulses with Envelopes\n", + "\n", + "Shaped pulses reduce spectral leakage and are essential for qubit control. The QICK supports:\n", + "- **Gaussian envelopes**: Smooth rise and fall\n", + "- **Flat-top pulses**: Gaussian edges with a constant top (ideal for readout resonators)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "0107848e31464010aaf97db383b65fab", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class ShapedPulseProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define a Gaussian envelope\n", + " # The envelope length should be sufficient to capture the full pulse\n", + " ramp_len = 0.15\n", + " self.add_gauss(\n", + " ch=cfg['gen_ch'],\n", + " name=\"gauss_env\",\n", + " sigma=ramp_len / 8, # Steepness of the Gaussian\n", + " length=ramp_len,\n", + " even_length=True\n", + " )\n", + "\n", + " # Gaussian pulse (arbitrary envelope)\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"gauss_pulse\",\n", + " style=\"arb\", envelope=\"gauss_env\",\n", + " freq=cfg['freq'], phase=cfg['phase'], gain=cfg['gain']\n", + " )\n", + "\n", + " # Flat-top pulse (good for readout)\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"flattop_pulse\",\n", + " style=\"flat_top\", envelope=\"gauss_env\",\n", + " freq=cfg['freq'], length=0.2, phase=cfg['phase'], gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"gauss_pulse\", t=0, tag='gauss')\n", + " self.pulse(ch=cfg['gen_ch'], name=\"flattop_pulse\", t=0.5, tag='flattop')\n", + "\n", + "config_shaped = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 100.0,\n", + " 'phase': 0,\n", + " 'gain': 0.8,\n", + " 'trig_time': 0.1,\n", + " 'ro_len': 1.0\n", + "}\n", + "\n", + "prog = ShapedPulseProgram(soccfg, reps=1, final_delay=0.5, cfg=config_shaped)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Separate the two pulses\n", + "magnitude = np.abs(iq_data[0].dot([1, 1j]))\n", + "\n", + "plt.figure(figsize=(12, 5))\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(time_axis, magnitude, label='Magnitude')\n", + "plt.axvline(x=0.25, color='r', linestyle='--', alpha=0.5, label='Gaussian peak')\n", + "plt.axvline(x=0.75, color='g', linestyle='--', alpha=0.5, label='Flat-top peak')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Magnitude (a.u.)')\n", + "plt.title('Shaped Pulses: Magnitude')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('Shaped Pulses: I/Q Components')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Key points about shaped pulses:**\n", + "- Gaussian pulses minimize spectral sidebands, reducing crosstalk\n", + "- Flat-top pulses maintain constant amplitude at the top, ideal for readout resonators\n", + "- The envelope is applied to the pulse shape, not the carrier frequency" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. IQ Mixing and Quadrature Signals\n", + "\n", + "The QICK uses IQ mixing to generate arbitrary phase shifts. I (in-phase) and Q (quadrature) components are 90° apart. By adjusting the phase parameter, you can rotate the pulse in the IQ plane." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "02b21b1a767d4b5b90b415cda53fe95b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class IQMixingProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Same pulse at different phases\n", + " for phase in cfg['phases']:\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=f\"pulse_{phase}\",\n", + " style=\"const\",\n", + " freq=cfg['freq'],\n", + " length=cfg['pulse_len'],\n", + " phase=phase,\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " for i, phase in enumerate(cfg['phases']):\n", + " self.pulse(ch=cfg['gen_ch'], name=f\"pulse_{phase}\", t=i * 0.3)\n", + "\n", + "config_iq = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0,\n", + " 'pulse_len': 0.1,\n", + " 'gain': 0.8,\n", + " 'trig_time': 0.05,\n", + " 'ro_len': 1.5,\n", + " 'phases': [0, 45, 90, 135, 180, 225, 270, 315]\n", + "}\n", + "\n", + "prog = IQMixingProgram(soccfg, reps=1, final_delay=0.5, cfg=config_iq)\n", + "iq_data = prog.acquire_decimated(soc, rounds=10)\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Extract magnitude at each pulse center\n", + "pulse_centers = [0.15 + i * 0.3 for i in range(len(config_iq['phases']))]\n", + "idx_centers = [np.argmin(np.abs(time_axis - t)) for t in pulse_centers]\n", + "iq_at_centers = iq_data[0][idx_centers, :]\n", + "\n", + "plt.figure(figsize=(12, 5))\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(time_axis, iq_data[0][:,0], label='I')\n", + "plt.plot(time_axis, iq_data[0][:,1], label='Q')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('IQ Mixing - Multiple Phases')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(iq_at_centers[:,0], iq_at_centers[:,1], 'o-', markersize=8)\n", + "for i, phase in enumerate(config_iq['phases']):\n", + " plt.annotate(f'{phase}°', (iq_at_centers[i,0], iq_at_centers[i,1]))\n", + "plt.axhline(0, color='k', linestyle='-', alpha=0.3)\n", + "plt.axvline(0, color='k', linestyle='-', alpha=0.3)\n", + "plt.xlabel('I')\n", + "plt.ylabel('Q')\n", + "plt.title('IQ Constellation')\n", + "plt.axis('equal')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation of IQ mixing:**\n", + "- Phase shifts rotate the pulse vector in the IQ plane\n", + "- A pulse with phase 0° appears on the I axis\n", + "- A pulse with phase 90° appears on the Q axis\n", + "- The constellation plot shows the relationship between I and Q for different phases" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Readout Resonator Characterization\n", + "\n", + "Readout resonators are used to measure qubit states. To optimize the readout, we need to find the resonator's resonant frequency. This is done by sweeping the readout frequency and measuring the response." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sweeping readout frequency...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f3d926ff7ce243bc92a757f482df9b04", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Resonance frequency: 509.50 MHz\n", + "Maximum magnitude: 391.372 a.u.\n" + ] + } + ], + "source": [ + "class ReadoutSweepProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define the readout pulse (flat-top recommended for readout)\n", + " ramp_len = 0.05\n", + " self.add_gauss(ch=cfg['gen_ch'], name=\"ramp\", \n", + " sigma=ramp_len/8, length=ramp_len, even_length=True)\n", + " \n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"readout_pulse\",\n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'], phase=0, gain=cfg['gain']\n", + " )\n", + "\n", + " # Configure readout - freq will be swept\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"readout_pulse\", t=0)\n", + "\n", + "# Frequency sweep parameters\n", + "freqs = np.linspace(490, 510, 41) # MHz\n", + "results_i = []\n", + "results_q = []\n", + "\n", + "base_config = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'pulse_len': 0.5,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.1,\n", + " 'ro_len': 0.8\n", + "}\n", + "\n", + "print(\"Sweeping readout frequency...\")\n", + "for f in freqs:\n", + " config = base_config.copy()\n", + " config['freq'] = f\n", + " prog = ReadoutSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + " iq_data = prog.acquire_decimated(soc, rounds=100) # 100 averages for clean data\n", + " \n", + " # Average the I/Q data over the pulse duration\n", + " # (take mean after the pulse has settled)\n", + " start_idx = int(0.2 / prog.get_time_axis(ro_index=0)[1]) # 0.2 us after trigger\n", + " i_avg = np.mean(iq_data[0][start_idx:, 0])\n", + " q_avg = np.mean(iq_data[0][start_idx:, 1])\n", + " results_i.append(i_avg)\n", + " results_q.append(q_avg)\n", + "\n", + "results_mag = np.sqrt(np.array(results_i)**2 + np.array(results_q)**2)\n", + "results_phase = np.arctan2(results_q, results_i) * 180 / np.pi\n", + "\n", + "plt.figure(figsize=(12, 5))\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(freqs, results_mag, 'o-', linewidth=2, markersize=4)\n", + "plt.xlabel('Frequency (MHz)')\n", + "plt.ylabel('|IQ| (a.u.)')\n", + "plt.title('Readout Resonator - Magnitude Response')\n", + "plt.grid(True)\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(freqs, results_phase, 'o-', linewidth=2, markersize=4)\n", + "plt.xlabel('Frequency (MHz)')\n", + "plt.ylabel('Phase (degrees)')\n", + "plt.title('Readout Resonator - Phase Response')\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Find the resonance frequency (minimum magnitude for a dispersive readout)\n", + "resonance_idx = np.argmin(results_mag)\n", + "resonance_freq = freqs[resonance_idx]\n", + "print(f\"\\nResonance frequency: {resonance_freq:.2f} MHz\")\n", + "print(f\"Maximum magnitude: {results_mag[resonance_idx]:.3f} a.u.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Interpreting the readout sweep:**\n", + "- The magnitude response shows a dip at the resonance frequency\n", + "- The phase response shows a sharp transition (dispersion) at resonance\n", + "- For optimal readout, you typically operate slightly detuned from resonance\n", + "- The exact optimal point depends on your measurement protocol (e.g., homodyne vs. heterodyne)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Optimizing Readout Parameters\n", + "\n", + "Once you find the resonance frequency, you can optimize other parameters like readout length and gain." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Optimizing readout length...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b26f9106cd774f9badc3296a9502900b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Optimal readout length: 1.10 µs\n", + "Maximum SNR: 7815.2\n" + ] + } + ], + "source": [ + "class ReadoutOptimizationProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " ramp_len = 0.05\n", + " self.add_gauss(ch=cfg['gen_ch'], name=\"ramp\", \n", + " sigma=ramp_len/8, length=ramp_len, even_length=True)\n", + " \n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"readout_pulse\",\n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'], phase=0, gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"readout_pulse\", t=0)\n", + "\n", + "# Use the found resonance frequency\n", + "optimal_freq = resonance_freq if 'resonance_freq' in dir() else 500.0\n", + "\n", + "# Sweep readout length\n", + "lengths = np.linspace(0.2, 2.0, 19)\n", + "snr_results = []\n", + "\n", + "print(\"Optimizing readout length...\")\n", + "for l in lengths:\n", + " config = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': optimal_freq,\n", + " 'pulse_len': l,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.1,\n", + " 'ro_len': l + 0.1 # Readout slightly longer than pulse\n", + " }\n", + " prog = ReadoutOptimizationProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + " iq_data = prog.acquire_decimated(soc, rounds=100)\n", + " \n", + " # Estimate SNR from signal magnitude\n", + " signal_mag = np.abs(iq_data[0].dot([1, 1j]))\n", + " signal_peak = np.max(signal_mag)\n", + " noise_std = np.std(signal_mag[:50]) # Noise before the pulse\n", + " snr = signal_peak / noise_std if noise_std > 0 else 0\n", + " snr_results.append(snr)\n", + "\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(lengths, snr_results, 'o-', linewidth=2, markersize=6)\n", + "plt.xlabel('Readout Pulse Length (µs)')\n", + "plt.ylabel('Signal-to-Noise Ratio (SNR)')\n", + "plt.title('Readout Optimization - SNR vs. Pulse Length')\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "optimal_idx = np.argmax(snr_results)\n", + "optimal_length = lengths[optimal_idx]\n", + "print(f\"\\nOptimal readout length: {optimal_length:.2f} µs\")\n", + "print(f\"Maximum SNR: {snr_results[optimal_idx]:.1f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "You have learned:\n", + "- How to configure generators with different pulse styles (constant, Gaussian, flat-top)\n", + "- How IQ mixing works and how phase shifts affect the IQ plane\n", + "- How to characterize a readout resonator via frequency sweeps\n", + "- How to optimize readout parameters like pulse length\n", + "\n", + "**Key takeaways:**\n", + "- Use shaped pulses (Gaussian or flat-top) for better spectral purity\n", + "- IQ mixing allows arbitrary phase control of your pulses\n", + "- Readout resonators should be characterized before experiments\n", + "- The optimal readout frequency is often slightly detuned from resonance\n", + "\n", + "**Next steps:** Proceed to [`07_Advanced_Generators_And_Readouts`](./07_Advanced_Generators_And_Readouts.ipynb) to learn about the DDR4 and MR buffers for high-speed data capture." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/07_Advanced_Generators_And_Readouts.ipynb b/docs/source/tutorials/07_Advanced_Generators_And_Readouts.ipynb new file mode 100644 index 000000000..de8474df5 --- /dev/null +++ b/docs/source/tutorials/07_Advanced_Generators_And_Readouts.ipynb @@ -0,0 +1,2174 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 07 - Advanced Generators and Readouts in tProc v2\n", + "\n", + "**Objective:** Explore the advanced generator and readout blocks available in the QICK firmware. This notebook covers:\n", + "\n", + "1. Muxed Generators (full-speed and mixer-mux) with Muxed Readouts (PFB)\n", + "2. Interpolated Generators vs. Standard Readouts (frequency matching in tProc v2 vs. v1)\n", + "3. Dynamically Configured Readouts for frequency sweeps and electrical delay calibration\n", + "4. Fast Dynamic Readout for phase-reset applications\n", + "5. IQ offset measurement and correction\n", + "6. Performance benchmarks\n", + "7. Real-time phase tracking\n", + "8. Common pitfalls and troubleshooting\n", + "9. Best practices summary\n", + "\n", + "**Prerequisites:** Complete the introductory tutorials ([`00_Getting_Started.ipynb`](./00_Getting_Started.ipynb) through [`06_Generators_And_Readouts.ipynb`](./06_Generators_And_Readouts.ipynb))." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Connected to QICK board\n", + "Available generators (DACs): [0, 1, 2, 3, 4]\n", + "Available readouts (ADCs): [0, 1, 2, 3, 4, 5, 6]\n", + "tProc core version: 2.0\n", + "Sofware version: 0.2.381\n" + ] + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "import time\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D, AsmV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Connect to the board (adjust path as needed)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define channel constants (adjust based on your firmware configuration)\n", + "FSGEN_CH = 0 # Full-speed generator (single tone, max bandwidth)\n", + "INTGEN_CH = [1, 2] # Interpolated generator (fine frequency resolution)\n", + "FSMUXGEN_CH = 3 # Full-speed mux generator (8 tones, phase coherent, up to 10 Gsps)\n", + "MIXMUXGEN_CH = 4 # Mixer-mux generator (8 tones, phase coherent, up to 7 Gsps, better SFDR)\n", + "\n", + "DYNRO_CH = 0 # Dynamically configured readout (axis_dyn_readout_v1)\n", + "FASTDYNRO_CH = 1 # Fast dynamic readout (axis_readout_v3, for phase-reset applications)\n", + "MUXRO_CH = [2, 3, 4, 5] # Muxed readout channels (PFB, 4 outputs used here)\n", + "STDRO_CH = 6 # Standard readout (axis_readout_v2, PYNQ-configured)\n", + "\n", + "print(\"Connected to QICK board\")\n", + "\n", + "print(f\"Available generators (DACs): {list(range(len(soccfg['gens'])))}\")\n", + "print(f\"Available readouts (ADCs): {list(range(len(soccfg['readouts'])))}\")\n", + "\n", + "print(f\"tProc core version: {soccfg['tprocs'][0]['version']}\")\n", + "print(f\"Sofware version: {soccfg['sw_version']}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Readouts (ADCs) Hardware Identification ===\n", + "RO 0:\n", + " - Readout Type: axis_dyn_readout_v1 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 1\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 1:\n", + " - Readout Type: axis_readout_v3 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 1\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 2:\n", + " - Readout Type: axis_pfb_readout_v3 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 2\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 3:\n", + " - Readout Type: axis_pfb_readout_v3 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 2\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 4:\n", + " - Readout Type: axis_pfb_readout_v3 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 2\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 5:\n", + " - Readout Type: axis_pfb_readout_v3 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 2\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n", + "RO 6:\n", + " - Readout Type: axis_readout_v2 (v1.0)\n", + " - Buffer Type: axis_avg_buffer\n", + " - Decimation: 1\n", + " - Sampling Freq: 2457.6 MHz\n", + "------------------------------\n" + ] + } + ], + "source": [ + "print(\"=== Readouts (ADCs) Hardware Identification ===\")\n", + "for i, ro in enumerate(soccfg['readouts']):\n", + " ro_kind = ro.get('ro_type', 'Unknown RO')\n", + " buf_kind = ro.get('avgbuf_type', 'Unknown Buffer')\n", + " ro_ver = ro.get('ro_version', 'N/A')\n", + " \n", + " print(f\"RO {i}:\")\n", + " print(f\" - Readout Type: {ro_kind} (v{ro_ver})\")\n", + " print(f\" - Buffer Type: {buf_kind}\")\n", + " print(f\" - Decimation: {ro.get('decimation', 'N/A')}\")\n", + " print(f\" - Sampling Freq: {ro.get('fs', 'N/A')} MHz\")\n", + " print(\"-\" * 30)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Generator and Readout Types Overview\n", + "\n", + "Understanding the different generator and readout types is crucial for optimal performance." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ GENERATOR TYPES ║\n", + "╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Generator Type ║ Best For ║\n", + "╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Full-Speed (FS) ║ Single tone, maximum bandwidth (10 Gsps), simple pulses ║\n", + "║ Interpolated (INT) ║ Fine frequency resolution, needs mixer_freq for upconversion, shaped pulses ║\n", + "║ Full-Speed Mux (FSMUX) ║ Multi-tone, phase-coherent, up to 8 tones @ 10 Gsps, constant envelope ║\n", + "║ Mixer-Mux (MIXMUX) ║ Multi-tone, phase-coherent, lower sample rate (7 Gsps), better SFDR ║\n", + "╠════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ READOUT TYPES ║\n", + "╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Readout Type ║ Best For ║\n", + "╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Standard (STD) ║ Fixed frequency readout, simple averaging ║\n", + "║ Dynamic (DYN) ║ Sweeping frequencies, resonator spectroscopy, delay calibration ║\n", + "║ Fast Dynamic (FASTDYN) ║ Phase-reset applications, fast sequencing, qubit readout ║\n", + "║ Muxed PFB (MUX) ║ Multi-tone readout, 64 channels, simultaneous demodulation ║\n", + "╚════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╝\n", + "\n" + ] + } + ], + "source": [ + "print(\"\"\"\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ GENERATOR TYPES ║\n", + "╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Generator Type ║ Best For ║\n", + "╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Full-Speed (FS) ║ Single tone, maximum bandwidth (10 Gsps), needs more PL logic ║\n", + "║ Interpolated (INT) ║ Fine frequency resolution, needs mixer_freq for upconversion, less PL logic ║\n", + "║ Full-Speed Mux (FSMUX) ║ Multi-tone, phase-coherent, up to 8 tones @ 10 Gsps, constant envelope ║\n", + "║ Mixer-Mux (MIXMUX) ║ Multi-tone, phase-coherent, lower sample rate (7 Gsps), better SFDR ║\n", + "╠════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ READOUT TYPES ║\n", + "╠════════════════════════╦═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Readout Type ║ Best For ║\n", + "╠════════════════════════╬═════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ Standard (STD) ║ Fixed frequency readout, simple averaging ║\n", + "║ Dynamic (DYN) ║ Sweeping frequencies, resonator spectroscopy, delay calibration ║\n", + "║ Fast Dynamic (FASTDYN) ║ Phase-reset applications, fast sequencing, qubit readout ║\n", + "║ Muxed PFB (MUX) ║ Multi-tone readout, 64 channels, simultaneous demodulation ║\n", + "╚════════════════════════╩═════════════════════════════════════════════════════════════════════════════════════╝\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Muxed Generators and Muxed Readouts (PFB)\n", + "\n", + "Muxed generators allow you to play multiple tones simultaneously, and muxed readouts (PFB) can demodulate them efficiently.\n", + "\n", + "**Key concepts:**\n", + "- The `mask` parameter in `add_pulse` selects which tones are played\n", + "- The PFB readout provides finer channel spacing than standard muxed readout (64 slices vs 8)\n", + "- Linking generators to readouts with `gen_ch` enables automatic frequency matching" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running muxed generator with PFB readout...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4e9e81cc098a4242b9175f40122dbef9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class MuxProgram(AveragerProgramV2):\n", + " \"\"\"\n", + " Program demonstrating muxed generator with PFB readout.\n", + " Plays multiple tones simultaneously on a single DAC channel.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_chs = cfg['ro_chs']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " # Declare a muxed generator with multiple tones\n", + " # The 'ro_ch' argument links it to a readout for frequency matching\n", + " self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_chs[0],\n", + " mixer_freq=cfg['mixer_freq'],\n", + " mux_freqs=cfg['pulse_freqs'], \n", + " mux_gains=cfg['pulse_gains'], \n", + " mux_phases=cfg['pulse_phases'])\n", + " \n", + " # Declare each PFB readout channel, linking back to the same generator\n", + " # This automatically matches readout frequencies to generator tones\n", + " for ch, f, ph in zip(cfg['ro_chs'], cfg['pulse_freqs'], cfg['ro_phases']):\n", + " self.declare_readout(ch=ch, length=cfg['ro_len'], freq=f, phase=ph, gen_ch=gen_ch)\n", + "\n", + " # Create a constant pulse that plays on all 4 tones simultaneously\n", + " # The 'mask' parameter selects which tone indices to enable\n", + " self.add_pulse(ch=gen_ch, name=\"mymux\", \n", + " style=\"const\", \n", + " length=cfg[\"pulse_len\"],\n", + " mask=[0, 1, 2, 3],\n", + " )\n", + " \n", + " def _body(self, cfg):\n", + " # Trigger readouts to start taking data before the pulse\n", + " self.trigger(ros=cfg['ro_chs'], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " # Play the pulse\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mymux\", t=0)\n", + " \n", + "# Configuration for the program\n", + "config_mux = {\n", + " 'gen_ch': MIXMUXGEN_CH, # we can use FSMUXGEN_CH here if we want higher sample rate but worse SFDR\n", + " 'ro_chs': MUXRO_CH,\n", + " 'mixer_freq': 500,\n", + " 'pulse_freqs': [500, 550, 600, 650], # MHz\n", + " 'pulse_gains': [1.0] * 4,\n", + " 'pulse_phases': [0.0] * 4,\n", + " 'ro_phases': [0.0] * 4, # Initial downconversion phases\n", + " 'trig_time': 0.4, # µs, trigger before pulse\n", + " 'pulse_len': 1.0, # µs\n", + " 'ro_len': 1.9, # µs, readout length\n", + "}\n", + "\n", + "print(\"Running muxed generator with PFB readout...\")\n", + "prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)\n", + "iq_list = prog.acquire_decimated(soc, rounds=1)\n", + "t = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot results for each readout channel\n", + "fig, axes = plt.subplots(len(config_mux['ro_chs']), 1, figsize=(12, 12))\n", + "for i, ch in enumerate(config_mux['ro_chs']):\n", + " axes[i].plot(t, iq_list[i][:, 0], label=\"I\")\n", + " axes[i].plot(t, iq_list[i][:, 1], label=\"Q\")\n", + " axes[i].plot(t, np.abs(iq_list[i].dot([1, 1j])), label=\"Magnitude\")\n", + " axes[i].legend()\n", + " axes[i].set_ylabel(f\"Tone {i} ({config_mux['pulse_freqs'][i]} MHz)\")\n", + " axes[i].set_xlabel(\"Time (µs)\")\n", + "plt.suptitle(\"Muxed Generator with PFB Readout\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Correcting IQ Phase Offsets\n", + "\n", + "The raw IQ data is rotated due to cable delays and mixer imperfections. We can correct this by measuring the average phase and applying a correction to either the upconversion (pulse) phase or the downconversion (readout) phase." + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Phase offsets: [-83.12613786622823, -130.43998631084776, 60.805937906847866, 15.380139289356636]\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "04e0d5bd756c4cf69aa97fe052e88ef5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Calculate average phase offset for each channel (in degrees)\n", + "phases = [np.angle(iq.mean(axis=0).dot([1, 1j]), deg=True) for iq in iq_list]\n", + "print(f\"Phase offsets: {phases}\")\n", + "\n", + "# Option 1: Rotate the upconversion phase\n", + "config_mux['pulse_phases'] = [-x for x in phases]\n", + "prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)\n", + "iq_list_corrected_up = prog.acquire_decimated(soc, rounds=1)\n", + "\n", + "# Option 2: Rotate the downconversion phase\n", + "config_mux['pulse_phases'] = [0.0] * 4\n", + "config_mux['ro_phases'] = phases\n", + "prog = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mux)\n", + "iq_list_corrected_down = prog.acquire_decimated(soc, rounds=1)\n", + "\n", + "# Plot corrected data (Option 2 as example)\n", + "fig, axes = plt.subplots(len(config_mux['ro_chs']), 1, figsize=(12, 10))\n", + "for i, ch in enumerate(config_mux['ro_chs']):\n", + " axes[i].plot(t, iq_list_corrected_down[i][:, 0], label=\"I\")\n", + " axes[i].plot(t, iq_list_corrected_down[i][:, 1], label=\"Q\")\n", + " axes[i].plot(t, np.abs(iq_list_corrected_down[i].dot([1, 1j])), label=\"Magnitude\")\n", + " axes[i].legend()\n", + " axes[i].set_ylabel(f\"Tone {i} (Phase-corrected)\")\n", + " axes[i].set_xlabel(\"Time (µs)\")\n", + "plt.suptitle(\"Phase-Corrected Muxed Readout\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Mixer-Mux Generator Example\n", + "\n", + "The mixer-mux generator uses a digital mixer-based architecture that provides better SFDR at the cost of lower sample rate." + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running mixer-mux generator...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3eb0ce4ba3c74f5e89df147ca29a732a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Using the same MuxProgram class with mixer-mux generator\n", + "config_mixmux = {\n", + " 'gen_ch': MIXMUXGEN_CH,\n", + " 'ro_chs': MUXRO_CH,\n", + " 'mixer_freq': 500,\n", + " 'pulse_freqs': [60, 100, 150, 400], # MHz (lower frequencies work better)\n", + " 'pulse_gains': [0.8] * 4, # Slightly lower gain for better linearity\n", + " 'pulse_phases': [0.0] * 4,\n", + " 'ro_phases': [0.0] * 4,\n", + " 'trig_time': 0.4,\n", + " 'pulse_len': 1.0,\n", + " 'ro_len': 1.9,\n", + "}\n", + "\n", + "print(\"Running mixer-mux generator...\")\n", + "prog_mixmux = MuxProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mixmux)\n", + "iq_list_mixmux = prog_mixmux.acquire_decimated(soc, rounds=1)\n", + "\n", + "# Plot results\n", + "fig, axes = plt.subplots(len(config_mixmux['ro_chs']), 1, figsize=(12, 10))\n", + "for i in range(len(config_mixmux['ro_chs'])):\n", + " axes[i].plot(t, np.abs(iq_list_mixmux[i].dot([1, 1j])), label=f\"Channel {i}\")\n", + " axes[i].set_ylabel(f\"Magnitude (Tone {i})\")\n", + " axes[i].set_xlabel(\"Time (µs)\")\n", + " axes[i].legend()\n", + "plt.suptitle(\"Mixer-Mux Generator Performance\")\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.3 Playing Different Tone Combinations\n", + "\n", + "You can selectively enable tones using different masks." + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running selective tone playback...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4aedf65f9fff4bae80630d1a19656083", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class MuxProgramSelective(AveragerProgramV2):\n", + " \"\"\"Program demonstrating selective tone playback using masks.\"\"\"\n", + " \n", + " def _initialize(self, cfg):\n", + " ro_chs = cfg['ro_chs']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_chs[0],\n", + " mixer_freq=cfg['mixer_freq'],\n", + " mux_freqs=cfg['pulse_freqs'], \n", + " mux_gains=cfg['pulse_gains'], \n", + " mux_phases=cfg['pulse_phases'])\n", + " \n", + " for ch, f, ph in zip(cfg['ro_chs'], cfg['pulse_freqs'], cfg['ro_phases']):\n", + " self.declare_readout(ch=ch, length=cfg['ro_len'], freq=f, phase=ph, gen_ch=gen_ch)\n", + "\n", + " # Add multiple pulses with different masks\n", + " self.add_pulse(ch=gen_ch, name=\"tones_even\", \n", + " style=\"const\", length=cfg[\"pulse_len\"],\n", + " mask=[0, 2]) # Even-indexed tones\n", + " \n", + " self.add_pulse(ch=gen_ch, name=\"tones_odd\", \n", + " style=\"const\", length=cfg[\"pulse_len\"],\n", + " mask=[1, 3]) # Odd-indexed tones\n", + " \n", + " def _body(self, cfg):\n", + " self.trigger(ros=cfg['ro_chs'], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " # First pulse\n", + " self.pulse(ch=cfg['gen_ch'], name=\"tones_even\", t=0)\n", + " # Second pulse starts after first finishes\n", + " self.pulse(ch=cfg['gen_ch'], name=\"tones_odd\", t=cfg['pulse_len'] + 0.1)\n", + "\n", + "config_selective = {\n", + " 'gen_ch': MIXMUXGEN_CH, # we can use FSMUXGEN_CH here if we want higher sample rate but worse SFDR\n", + " 'ro_chs': MUXRO_CH[:4],\n", + " 'mixer_freq': 500,\n", + " 'pulse_freqs': [50, 150, 200, 350],\n", + " 'pulse_gains': [1.0] * 4,\n", + " 'pulse_phases': [0.0] * 4,\n", + " 'ro_phases': [0.0] * 4,\n", + " 'trig_time': 0.4,\n", + " 'pulse_len': 1.0,\n", + " 'ro_len': 2.5,\n", + "}\n", + "\n", + "print(\"Running selective tone playback...\")\n", + "prog_selective = MuxProgramSelective(soccfg, reps=1, final_delay=0.5, cfg=config_selective)\n", + "iq_list_selective = prog_selective.acquire_decimated(soc, rounds=1)\n", + "\n", + "npts = iq_list_selective[0].shape[0]\n", + "t = np.linspace(0, config_selective['ro_len'], npts)\n", + "\n", + "# Plot showing two pulse sequences\n", + "npts = iq_list_selective[0].shape[0]\n", + "t = np.linspace(0, config_selective['ro_len'], npts)\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(12, 10))\n", + "\n", + "for i, ch in enumerate(config_selective['ro_chs']):\n", + " mag = np.abs(iq_list_selective[i].dot([1, 1j]))\n", + " axes[0].plot(t, mag, label=f\"Tone {i} ({config_selective['pulse_freqs'][i]} MHz)\")\n", + "\n", + "axes[0].set_ylabel(\"Magnitude\")\n", + "axes[0].set_xlabel(\"Time (µs)\")\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Selective Tone Playback: Even tones first, then odd tones\")\n", + "axes[0].grid(True)\n", + "\n", + "for i in range(4):\n", + " mag = np.abs(iq_list_selective[i].dot([1, 1j]))\n", + " axes[1].plot(t, mag, label=f\"Tone {i}\")\n", + "\n", + "axes[1].set_ylabel(\"Magnitude\")\n", + "axes[1].set_xlabel(\"Time (µs)\")\n", + "axes[1].legend()\n", + "axes[1].set_title(\"Individual Tone Responses\")\n", + "axes[1].grid(True)\n", + "\n", + "plt.tight_layout()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Interpolated Generator and Standard Readout\n", + "\n", + "The interpolated generator uses a digital mixer to upconvert an IF signal. \n", + "\n", + "### Frequency Matching in tProc v2 vs. tProc v1\n", + "\n", + "| Aspect | tProc v1 | tProc v2 |\n", + "|--------|----------|----------|\n", + "| Frequency specification | IF (intermediate frequency) | Absolute frequency |\n", + "| Mixer frequency handling | User must add to both gen and readout | Automatic |\n", + "| Common point of confusion | Forgetting to account for mixer | None (automatic) |\n", + "\n", + "In tProc v2, you specify the **absolute** frequency you want to play. The software automatically calculates the correct IF by subtracting the mixer frequency. The readout frequency is also absolute." + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running interpolated generator with parameter sweeps...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5a71a774629346ed9e15f6a93e995ab3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class SimpleSweepProgram(AveragerProgramV2):\n", + " \"\"\"\n", + " Program demonstrating interpolated generator with parameter sweeps.\n", + " Sweeps phase and gain simultaneously across multiple steps.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " # Declare an interpolated generator with a mixer frequency\n", + " self.declare_gen(ch=gen_ch, nqz=1, ro_ch=ro_ch, mixer_freq=cfg['mixer_freq'])\n", + " \n", + " # Declare a standard readout (frequency is absolute)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'], freq=cfg['freq'], gen_ch=gen_ch)\n", + "\n", + " # Add a loop for sweeping parameters\n", + " self.add_loop(\"myloop\", self.cfg[\"steps\"])\n", + "\n", + " # Create a flat-top pulse with Gaussian ramps\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", sigma=cfg['ramp_len']/10, \n", + " length=cfg['ramp_len'], even_length=True)\n", + " self.add_pulse(ch=gen_ch, name=\"mypulse\", ro_ch=ro_ch, \n", + " style=\"flat_top\", envelope=\"ramp\", \n", + " freq=cfg['freq'], length=cfg['flat_len'],\n", + " phase=cfg['phase'], gain=cfg['gain'])\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mypulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Configuration\n", + "config_sweep = {\n", + " 'steps': 5,\n", + " 'gen_ch': INTGEN_CH[0],\n", + " 'ro_ch': STDRO_CH,\n", + " 'freq': 2000, # Absolute frequency (MHz)\n", + " 'mixer_freq': 1500, # Mixer frequency (MHz)\n", + " 'trig_time': 0.45, # µs\n", + " 'ro_len': 0.3, # µs\n", + " 'flat_len': 0.05, # µs\n", + " 'ramp_len': 0.2, # µs (total pulse length is flat_len + ramp_len)\n", + " 'phase': QickSweep1D(\"myloop\", -360, 720), # Sweep phase from -360° to 720°\n", + " 'gain': QickSweep1D(\"myloop\", 0.0, 1.0) # Sweep gain from 0 to 1\n", + "}\n", + "\n", + "print(\"Running interpolated generator with parameter sweeps...\")\n", + "prog = SimpleSweepProgram(soccfg, reps=1, final_delay=0.5, cfg=config_sweep)\n", + "iq_list = prog.acquire_decimated(soc, rounds=1)\n", + "t = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Plot results\n", + "plt.figure(figsize=(12, 6))\n", + "for ii, iq in enumerate(iq_list[0]):\n", + " plt.plot(t, iq[:, 0], label=f\"I value, step {ii}\")\n", + "plt.legend()\n", + "plt.ylabel(\"Amplitude\")\n", + "plt.xlabel(\"Time (µs)\")\n", + "plt.title(\"Swept Phase and Gain with Interpolated Generator\")\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Full-Speed Generator and Dynamically Configured Readout\n", + "\n", + "Dynamically configured readouts allow you to change parameters (like frequency) during a sweep. This is essential for resonator spectroscopy and electrical delay calibration.\n", + "\n", + "**Key steps:**\n", + "1. Define a readout configuration using `add_readoutconfig()`\n", + "2. Send the configuration using `send_readoutconfig()` before it takes effect\n", + "3. For sweeps, send the new configuration in the `exec_before` block of the loop" + ] + }, + { + "cell_type": "code", + "execution_count": 54, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running dynamic frequency sweep...\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8c845cef52394218acd21f18f1e10101", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/50 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class DynamicFreqSweepProgram(AveragerProgramV2):\n", + " \"\"\"\n", + " Program demonstrating dynamic readout configuration for frequency sweeps.\n", + " Used for electrical delay calibration and resonator characterization.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=cfg['nqz'])\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define the readout configuration (like a pulse for the readout)\n", + " self.add_readoutconfig(ch=ro_ch, name=\"myro\", freq=cfg['freq'], gen_ch=gen_ch)\n", + " \n", + " # The exec_before block runs before each loop iteration\n", + " # We use it to send the updated readout config\n", + " loopbefore = AsmV2()\n", + " loopbefore.send_readoutconfig(ch=cfg['ro_ch'], name=\"myro\", t=0)\n", + " self.add_loop(\"myloop\", self.cfg[\"steps\"], exec_before=loopbefore)\n", + "\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", sigma=cfg['ramp_len']/10, \n", + " length=cfg['ramp_len'], even_length=True)\n", + " self.add_pulse(ch=gen_ch, name=\"mypulse\", ro_ch=ro_ch, \n", + " style=\"flat_top\", envelope=\"ramp\", \n", + " freq=cfg['freq'], length=cfg['flat_len'],\n", + " phase=cfg['phase'], gain=cfg['gain'])\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mypulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "# Small frequency sweep to see phase rotation\n", + "START_FREQ = 100\n", + "CAL_RANGE = 1\n", + "\n", + "config_dyn = {\n", + " 'steps': 5,\n", + " 'gen_ch': FSGEN_CH,\n", + " 'ro_ch': DYNRO_CH,\n", + " 'freq': QickSweep1D(\"myloop\", START_FREQ, START_FREQ + CAL_RANGE),\n", + " 'nqz': 1,\n", + " 'trig_time': 0.4, # µs\n", + " 'ro_len': 0.3, # µs\n", + " 'flat_len': 0.05, # µs\n", + " 'ramp_len': 0.2, # µs\n", + " 'phase': 0,\n", + " 'gain': 1.0\n", + "}\n", + "\n", + "print(\"Running dynamic frequency sweep...\")\n", + "prog = DynamicFreqSweepProgram(soccfg, reps=10, final_delay=0.5, cfg=config_dyn)\n", + "iq_list = prog.acquire(soc, rounds=1)\n", + "freqs = prog.get_pulse_param('myro', 'freq', as_array=True)\n", + "\n", + "# The IQ vector rotates as frequency changes due to electrical delay\n", + "plt.figure(figsize=(8, 8))\n", + "plt.plot(iq_list[0][0][:, 0], iq_list[0][0][:, 1], '.-', markersize=10)\n", + "plt.ylabel(\"Q\")\n", + "plt.xlabel(\"I\")\n", + "plt.title(\"Phase Rotation from Small Frequency Sweep\")\n", + "plt.axis('equal')\n", + "plt.grid(True)\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.1 Electrical Delay Calibration\n", + "\n", + "The phase rotation we observe is due to the electrical delay between the DAC and ADC. By measuring the slope of phase vs. frequency, we can calculate this delay and correct for it." + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Performing frequency sweep for delay calibration...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9c431a2815a0470fbb8e77232387f3d9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100100 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "# Perform a wide-range sweep for calibration\n", + "END_FREQ = 8000 # MHz\n", + "config_dyn['freq'] = QickSweep1D(\"myloop\", START_FREQ, END_FREQ)\n", + "config_dyn['steps'] = 1001 # Number of frequency points\n", + "\n", + "print(\"Performing frequency sweep for delay calibration...\")\n", + "prog = DynamicFreqSweepProgram(soccfg, reps=100, final_delay=1.0, cfg=config_dyn)\n", + "freqs = prog.get_pulse_param('myro', 'freq', as_array=True)\n", + "\n", + "# Acquire data with progress bar\n", + "start_time = time.time()\n", + "iq_list = prog.acquire(soc, rounds=1, progress=True)\n", + "iq_complex = iq_list[0][0].dot([1, 1j])\n", + "print(f\"Sweep completed in {time.time() - start_time:.2f} seconds\")\n", + "\n", + "# Measure the phase slope (electrical delay)\n", + "phases = np.unwrap(np.angle(iq_complex)) / (2 * np.pi)\n", + "a = np.vstack([freqs, np.ones_like(freqs)]).T\n", + "phase_delay = np.linalg.lstsq(a, phases, rcond=None)[0][0]\n", + "print(f\"Measured electrical delay: {phase_delay:.6f} µs\")\n", + "print(f\"This corresponds to {phase_delay * 1e3:.2f} ns of cable delay\")\n", + "\n", + "# Apply the correction\n", + "iq_rotated = iq_complex * np.exp(-1j * freqs * 2 * np.pi * phase_delay)\n", + "\n", + "# Plot the corrected IQ trajectory\n", + "plt.figure(figsize=(8, 8))\n", + "plt.plot(np.real(iq_rotated), np.imag(iq_rotated), '.')\n", + "plt.ylabel(\"Q\")\n", + "plt.xlabel(\"I\")\n", + "plt.title(\"Corrected IQ Trajectory (Flat Phase)\")\n", + "plt.axis('equal')\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "# Plot amplitude and phase of the corrected data\n", + "phases_corrected = np.unwrap(np.angle(iq_rotated)) / (2 * np.pi)\n", + "\n", + "fig, ax1 = plt.subplots(figsize=(12, 6))\n", + "ax1.semilogy(freqs, np.abs(iq_rotated), label=\"Amplitude\", color='blue')\n", + "ax1.set_ylabel(\"Amplitude\", color='blue')\n", + "ax1.set_xlabel(\"Frequency (MHz)\")\n", + "ax1.tick_params(axis='y', labelcolor='blue')\n", + "ax1.grid(True)\n", + "\n", + "ax2 = ax1.twinx()\n", + "ax2.plot(freqs, phases_corrected, color='red', label='Phase')\n", + "ax2.set_ylabel(\"Phase (cycles)\", color='red')\n", + "ax2.tick_params(axis='y', labelcolor='red')\n", + "\n", + "plt.title(\"Frequency Response After Delay Calibration\")\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Fast Dynamic Readout (Phase-Reset Applications)\n", + "\n", + "The fast dynamic readout (`axis_readout_v3`) is optimized for applications requiring rapid phase resets between measurements, such as qubit readout in quantum computing experiments. It allows changing readout parameters on every trigger without reconfiguring the entire readout chain." + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " def add_readoutconfig(self, ch, name, **kwargs):\n", + " \"\"\"Add a readout config to the program's pulse library.\n", + " The \"mode\" and \"length\" parameters have no useful effect and should probably never be used.\n", + "\n", + " Parameters\n", + " ----------\n", + " ch : int or list of int\n", + " Readout channel (index in 'readouts' list).\n", + " The use of one readoutconfig for multiple readouts is experimental.\n", + " The readouts must be of the same type and running at the same sampling frequency.\n", + " name : str\n", + " name of the config\n", + " freq : float or QickParam\n", + " Frequency (MHz)\n", + " phase : float or QickParam\n", + " Phase (degrees)\n", + " phrst : int\n", + " If 1, it resets the DDS phase. The default is 0.\n", + " mode : str\n", + " Selects whether the output is \"oneshot\" (the default) or \"periodic.\"\n", + " outsel : str\n", + " Selects the output source. The input is real, the output is complex. If \"product\" (the default), the output is the product of input and DDS. If \"dds\", the output is the DDS only. If \"input\", the output is from the input. If \"zero\", the output is always zero.\n", + " length : float or QickParam\n", + " The duration (us) of the config pulse. The default is the shortest possible length.\n", + " gen_ch : int\n", + " generator channel (use None if you don't want the downconversion frequency to be rounded to a valid DAC frequency or be offset by the DAC mixer frequency)\n", + " \"\"\"\n", + " if isinstance(ch, Number):\n", + " ch = [ch]\n", + " else: # list of channels\n", + " distinct_cfgs = len(set([self._ro_mgrs[x].cfg_hash for x in ch]))\n", + " if distinct_cfgs != 1:\n", + " raise RuntimeError(\"tried to define a pulse for generators %s, but they have different types or sampling freqs\"%(ch))\n", + " prog_chs = set(self.ro_chs.keys())\n", + " if set(ch) - prog_chs:\n", + " raise RuntimeError(\"readoutconfig %s is defined for readout(s) %s, but only readouts %s are declared\"%(name, ch, prog_chs))\n", + " pulse = self._ro_mgrs[ch[0]].make_pulse(kwargs)\n", + " pulse.ro_chs = [ch]\n", + " self._register_pulse(pulse, name)\n", + "\n" + ] + } + ], + "source": [ + "import inspect\n", + "from qick.qick_asm import AbsQickProgram\n", + "print(inspect.getsource(prog_fast.add_readoutconfig))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Running fast dynamic readout test (frequency hopping)...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "aa5c116b68a44245b75db86ee005b254", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/100 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class FastDynamicReadoutProgram(AveragerProgramV2):\n", + " \"\"\"\n", + " Program demonstrating fast dynamic readout for rapid parameter changes.\n", + " Ideal for phase-reset applications and fast sequencing.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=cfg['nqz'])\n", + " \n", + " # declare_readout: length only, no sel/outsel/freq/phase\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + " \n", + " # outsel goes here in add_readoutconfig only\n", + " for i, (freq, phase) in enumerate(zip(cfg['freqs'], cfg['phases'])):\n", + " self.add_readoutconfig(\n", + " ch=ro_ch,\n", + " name=f\"ro_conf_{i}\",\n", + " freq=freq,\n", + " phase=phase,\n", + " gen_ch=gen_ch,\n", + " outsel='input'\n", + " )\n", + " \n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", sigma=cfg['ramp_len']/10,\n", + " length=cfg['ramp_len'], even_length=True)\n", + " \n", + " for i, (freq, phase) in enumerate(zip(cfg['freqs'], cfg['phases'])):\n", + " self.add_pulse(ch=gen_ch,\n", + " name=f\"pulse_{i}\",\n", + " ro_ch=ro_ch,\n", + " style=\"flat_top\",\n", + " envelope=\"ramp\",\n", + " freq=freq,\n", + " phase=phase,\n", + " length=cfg['flat_len'],\n", + " gain=cfg['gain']\n", + " )\n", + " \n", + " def _body(self, cfg):\n", + " t = 0 # running absolute time cursor\n", + " step_len = cfg['ro_len'] + 0.1 # readout window + small guard gap\n", + " \n", + " for i in range(cfg['num_steps']):\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=f\"ro_conf_{i}\", t=t)\n", + " self.pulse(ch=cfg['gen_ch'], name=f\"pulse_{i}\", t=t)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=t + cfg['trig_time'])\n", + " \n", + " # Advance time cursor past this step's readout window\n", + " self.delay_auto(t=0.1, tag=f\"step_{i}\") # let QICK resolve end of step\n", + " t += step_len # manually track for next pulse/trigger offset\n", + " \n", + "\n", + "# Example: Fast frequency hopping\n", + "config_fast = {\n", + " 'gen_ch': FSGEN_CH,\n", + " 'ro_ch': FASTDYNRO_CH,\n", + " 'nqz': 1,\n", + " 'freqs': [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000],\n", + " 'phases': [0] * 10,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.4,\n", + " 'ro_len': 5.0,\n", + " 'flat_len': 4.5,\n", + " 'ramp_len': 0.2,\n", + " 'num_steps': 10,\n", + " 'final_delay': 0.5, # per-step gap, passed to delay_auto\n", + "}\n", + "\n", + "print(\"Running fast dynamic readout test (frequency hopping)...\")\n", + "prog_fast = FastDynamicReadoutProgram(soccfg, reps=100, final_delay=0.5, cfg=config_fast)\n", + "iq_list_fast = prog_fast.acquire(soc, rounds=1, progress=True)\n", + "\n", + "iq_array = iq_list_fast[0] # shape (10, 2)\n", + "\n", + "magnitudes = np.abs(iq_array[:, 0] + 1j * iq_array[:, 1])\n", + "\n", + "# Plot results for each frequency\n", + "plt.figure(figsize=(8, 5))\n", + "plt.plot(config_fast['freqs'], magnitudes, 'o-')\n", + "plt.xlabel(\"Frequency (MHz)\")\n", + "plt.ylabel(\"Magnitude (averaged IQ)\")\n", + "plt.title(\"Fast Dynamic Readout - Frequency Hopping\")\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Measuring IQ Offsets\n", + "\n", + "Some readout blocks (especially muxed ones) introduce a small, systematic DC offset. The `acquire()` and `acquire_decimated()` methods automatically subtract these offsets from the returned data. The raw data (without subtraction) can be accessed via `get_raw()`." + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Measuring IQ offsets...\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b49e458d22634c9099ecabe656a98d4f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1000 [00:00\n", + "ch 3 iq_offset: -0.5, type: \n", + "ch 4 iq_offset: -0.5, type: \n", + "ch 5 iq_offset: -0.5, type: \n", + "ch 6 iq_offset: 0.0, type: \n", + "\n", + "Note: The software automatically subtracts these hardware offsets from acquired data.\n", + "You can manually access raw data via prog.get_raw() if needed.\n" + ] + }, + { + "data": { + "image/png": "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CgrYunVrTPuIp3Xr1lFYWEiPHj3YsGEDM2bMoFu3blx++eX12s/OnTt57LHHGlSHuXPn0rVrV7p27crcuXMjbjNnzhzat29PYWEhhYWFzJo1q87yGzdupHfv3nTt2pURI0awf//+BtUvnBIsERHJTD/4QeghIpLGDhw4kPBjHDx4MKbyL7zwAsOGDeOvf/0rXbp04bHHHmPhwoXMnz+/XvtpaIK1fft2Jk+ezMqVK3njjTeYPHkyO3bsiLjtiBEjKCsro6ysjO985zt1lh8/fjw33XQT69evp23btjzxxBP1rl91GiIoIiKSRuqasa82um9MJL28+OKL3H333ezfv5927doxf/58jj76aCZNmsSHH37I+++/T15eHg8//DCjRo1iy5YtnHHGGbg7AFOmTCEnJ4cbb7yRm266iTfffJMlS5bwyiuvMHv2bObNm8f111/PqlWr+Oyzz7j00kuZPHkyEOoFu+qqqygpKWHs2LHk5uZy5513sm/fPrp06cLs2bMP630rKytjzJgx7Nmzhy5duvDkk0/y2muvMX36dJo0acLy5cs54YQTeO+99yguLuaqq66iZ8+efP/73wdC9yotX76c1q1bM3XqVJ555hn27dvHxRdfzOTJk5kwYQIbNmygsLCQQYMGMXXq1Kji+Mc//pFBgwaRm5sLwKBBg3jppZcYNWpUTOVHjhzJkiVL+OUvfwnA6NGjmTRpEtdff31U+62JEiwRERERyQwDBx6+7LLL4LvfhT174BvfOHz9lVeGHlu3wqWXVl23bFmdh/zss88oLCysfL99+3aKi4sB6NevH6+//jpmxqxZs5gyZQrTpk0DYPXq1axYsYIWLVpw44030q9fPyZOnMgf/vAHZs6cCcBZZ53FtGnTuPHGGyktLWXfvn18/vnnrFixgv79+wNwzz33kJuby8GDBznnnHN466236N69OxD6/aYVK1awdetWLrnkEhYvXkzLli257777eOCBB5g4cWKVc7niiit4+OGHGTBgABMnTmTy5MlMnz6dMWPG0KpVK374wx8C8NJLL7F06VLy8vIYOnQojz76KEVFRezevZucnBxKSkpYv349b7zxBu5OcXExy5cv59577+Xvf/87ZWHDtwsLC6u8j2TTpk106tSp8n1+fj6bNm2KuO1zzz3H8uXLOf7443nwwQfp1KlTjeW3bdvGkUceSdOmTevcb30owRIREZGYNLTXTT1u8aHZMFOrRYsWVRKEOXPmUFpaCoR+p2vEiBFUVFSwf//+Kr+jVFxcTIsWLQBYvnw5zz//PAAXXHABbdu2BeC0005j9erV7Nq1i+bNm9OzZ09KS0t59dVXmTFjBgDPPPMMM2fO5MCBA1RUVLBmzZrKBGvEiBEAvP7666xZs4aioiIA9u/fT9++faucxyeffMLOnTsZMGAAEOrNGT58eJ3nX1RUxLhx47j88su55JJLyM/Pp6SkhJKSEnr06AHA7t27Wb9+Pcccc8xh5etKroDKHr1wkWb2Gzp0KKNGjaJ58+Y8/vjjjB49miVLltRYPtr91pcSLBERERHJDLX1OH35y7Wvz8uLqseqPm644QbGjRtHcXExy5YtY9KkSZXrWrZsWWXbSB/smzVrRkFBAbNnz+bMM8+ke/fuLF26lA0bNtCtWzc2btzI/fffz6pVq2jbti1XXnllld9rOnQMd2fQoEE8/fTTcT0/gAkTJnDBBRewcOFC+vTpw+LFi3F3brnlFq677roq277//vtR7XPlypWVZe+66y7y8/NZFvZvU15ezsAIvZXt2rWrfH3NNdcwfvx4gBrL5+XlsXPnTg4cOEDTpk0pLy/nq1/9anQnXgslWCIikpl+8pNU16BRieXer3TU2Hp16op/OtZZ6vbJJ5/QsWNHgBpnvoPQUMD58+dz++23s2jRoioTOJx11lncf//9PPnkk5xyyimMGzeO0047DTPj008/pWXLlrRp04bNmzezaNGiiIlHnz59+N73vse7777L1772Nfbs2UN5eTnHH3985TZt2rShbdu2vPrqq/Tv35+nnnqqsjerNhs2bOCUU07hlFNO4bXXXmPdunUMHjyYO+64g8svv5xWrVqxadMmmjVrRuvWrdm1a1ed++zdu3eVnq3t27dz6623VsalpKSEn/70p4eVq6iooEOHDgAsWLCAbt26ATB48OCI5c2Ms88+m2effZaRI0cyd+5chg0bVmf96tLgBMvMOgG/AL4CfAHMdPeHzCwX+DVQALwPXObukaf5EBERSZQzz0x1DUQky02aNInhw4fTsWNH+vTpw8aNGyNud+eddzJq1Ch69uzJgAEDqgyl69+/P/fccw99+/alZcuW5OTkVN5/deqpp9KjRw9OOukkjjvuuMohgNW1b9+eOXPmMGrU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6wkNaTwVeD8o801gsLvXJ0bfBt43s0+BMYTuTcPdSwndh/UIoeGG7wJXhpX7KaGkcGf4zIMiIpJ8FvqyTkRERCIxs/OAp4Fz3L0sxdUREZE0pwRLRESkDmY2FOjo7pEmxhAREamkBEtERERERCROdA+WiIiIiIhInNT1WyhJkZeX5wUFBamuhoiIiIiISFRWr1691d0P+wH6tEiwCgoKKC0tTXU1REQkk7zzTuj5hBNSWw8REclIZvbPSMvTIsESERGJu+uuCz0vW5bSaoiISHZRgiUiIiKNztVzVtW6/okrT09STUREqtIkFyIiIiIiInGiHiwRERHJOLX1cKl3K3N8/vnnlJeXs3fv3lRXRTJYTk4O+fn5NGvWLKrtlWCJiIiISKNUXl5O69atKSgowMxSXR3JQO7Otm3bKC8vp3PnzlGVUYIlIiKZ6fbbU10DEUmwvXv3KrmShDIz2rVrx5YtW6IuowRLREQy07nnproGIpIESq4k0ep7jWmSCxERyUxlZaGHiIhIEinBEhGRzPSDH4QeIiIJ1KpVqyrv58yZw9ixY2PaZ0FBAVu3bo1pH/G0bt06CgsL6dGjBxs2bGDGjBl069aNyy+/vF772blzJ4899liD6jB37ly6du1K165dmTt3bsRt5syZQ/v27SksLKSwsJBZs2bVWX7jxo307t2brl27MmLECPbv39+g+oXTEEERERHRrHsiKXLgwAGaNk3sR/KDBw/SpEmTBpd/4YUXGDZsGJMnTwbgscceY9GiRVFP+nDIoQTru9/9br3Kbd++ncmTJ1NaWoqZcdppp1FcXEzbtm0P23bEiBE88sgjUZcfP348N910EyNHjmTMmDE88cQTXH/99fWqX3XqwRIRERERSYAXX3yR3r1706NHD84991w2b94MwKRJk7j22ms577zzuOKKK9i2bRvnnXcePXr04LrrrsPdAZgyZQozZswA4KabbuLrX/86AK+88grf+ta3ALj++uvp1asXJ510EnfeeWflsQsKCrjrrrvo168fv/nNbygpKaFv37707NmT4cOHs3v37sPqW1ZWRp8+fejevTsXX3wxO3bsYOHChUyfPp1Zs2Zx9tlnM2bMGN577z2Ki4t58MEH+dOf/lTZY9SjRw927doFwNSpUzn99NPp3r17Zb0mTJjAhg0bKCws5Oabb446jn/84x8ZNGgQubm5tG3blkGDBvHSSy/FXN7dWbJkCZdeeikAo0eP5oUXXoh6vzVRD5aIiEgjUVsvE6inSYSBAw9fdtll8N3vwp498I1vHL7+yitDj61bIfigXWnZsjoP+dlnn1FYWFj5fvv27RQXFwPQr18/Xn/9dcyMWbNmMWXKFKZNmwbA6tWrWbFiBS1atODGG2+kX79+TJw4kT/84Q/MnDkTgLPOOotp06Zx4403Ulpayr59+/j8889ZsWIF/fv3B+Cee+4hNzeXgwcPcs455/DWW2/RvXt3IPT7TStWrGDr1q1ccsklLF68mJYtW3LffffxwAMPMHHixCrncsUVV/Dwww8zYMAAJk6cyOTJk5k+fTpjxoyhVatW/PCHPwTgpZdeYunSpeTl5TF06FAeffRRioqK2L17Nzk5OZSUlLB+/XreeOMN3J3i4mKWL1/Ovffey9///nfKwu6PLSwsrPI+kk2bNtGpU6fK9/n5+WzatCnits899xzLly/n+OOP58EHH6RTp041lt+2bRtHHnlkZQ9ibfutDyVYIiIiIiIN1KJFiyoJwpw5cygtLQVCv9M1YsQIKioq2L9/f5UhdcXFxbRo0QKA5cuX8/zzzwNwwQUXVA59O+2001i9ejW7du2iefPm9OzZk9LSUl599dXKnq1nnnmGmTNncuDAASoqKlizZk1lgjVixAgAXn/9ddasWUNRUREA+/fvp2/fvlXO45NPPmHnzp0MGDAACPXmDB8+vM7zLyoqYty4cVx++eVccskl5OfnU1JSQklJCT169ABg9+7drF+/nmOOOeaw8nUlV0Blj164SDP7DR06lFGjRtG8eXMef/xxRo8ezZIlS2osH+1+60sJloiIZKaf/CTVNRB0b1c4xSIJautx+vKXa1+flxdVj1V93HDDDYwbN47i4mKWLVvGpEmTKte1bNmyyraRPtg3a9aMgoICZs+ezZlnnkn37t1ZunQpGzZsoFu3bmzcuJH777+fVatW0bZtW6688kr27t172DHcnUGDBvH000/H9fwgNOzvggsuYOHChfTp04fFixfj7txyyy1cd911VbZ9//33o9rnypUrK8vedddd5Ofnsyzs36a8vJyBEXor27VrV/n6mmuuYfz48QA1ls/Ly2Pnzp2V98GVl5fz1a9+NboTr4XuwRIRkcx05pmhh4hIinzyySd07NgRoMaZ7yA0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YImIiIiIiMRJWvwOVl5enhcUFKS6GiIiIiIiIlFZvXr1VndvX315WiRYBQUFlJaWproaIiKSSd55J/R8wgmprYeIiGQkM/tnpOVpkWCJiIjE3XXXhZ6XLUtpNUREJLsowRIREREJXD1nVa3rn7jy9CTVREQaK01yISIiIiIiEifqwRIREYmzTOsFybTziUVtscimOKSLzz//nPLycvbu3ZvqqkgGy8nJIT8/n2bNmkW1vRIsEREREWmUysvLad26NQUFBZhZqqsjGcjd2bZtG+Xl5XTu3DmqMkqwREQkM91+e6prICIJtnfvXiVXklBmRrt27diyZUvUZZRgiYhIZjr33Do30XAvkcZPyZUkWn2vMU1yISIimamsLPQQERFJIiVYIiKSmX7wg9BDRCSBWrVqVeX9nDlzGDt2bEz7LCgoYOvWrTHtI57WrVtHYWEhPXr0YMOGDcyYMYNu3bpx+eWX12s/O3fu5LHHHmtQHebOnUvXrl3p2rUrc+fOjbjNnDlzaN++PYWFhRQWFjJr1qw6y2/cuJHevXvTtWtXRowYwf79+xtUv3BKsEREREREUuTAgQMJP8bBgwdjKv/CCy8wbNgw/vrXv9KlSxcee+wxFi5cyPz58+u1n4YmWNu3b2fy5MmsXLmSN954g8mTJ7Njx46I244YMYKysjLKysr4zne+U2f58ePHc9NNN7F+/Xratm3LE088Ue/6Vad7sERERNJIXVOi10b3jYmklxdffJG7776b/fv3065dO+bPn8/RRx/NpEmT+PDDD3n//ffJy8vj4YcfZtSoUWzZsoUzzjgDdwdgypQp5OTkcOONN3LTTTfx5ptvsmTJEl555RVmz57NvHnzuP7661m1ahWfffYZl156KZMnTwZCvWBXXXUVJSUljB07ltzcXO6880727dtHly5dmD179mG9b2VlZYwZM4Y9e/bQpUsXnnzySV577TWmT59OkyZNWL58OSeccALvvfcexcXFXHXVVfTs2ZPvf//7QOhepeXLl9O6dWumTp3KM888w759+7j44ouZPHkyEyZMYMOGDRQWFjJo0CCmTp0aVRz/+Mc/MmjQIHJzcwEYNGgQL730EqNGjYqp/MiRI1myZAm//OUvARg9ejSTJk3i+uuvj2q/NVGCJSIiIiKZYeDAw5dddhl897uwZw984xuHr7/yytBj61a49NKq65Ytq/OQn332GYWFhZXvt2/fTnFxMQD9+vXj9ddfx8yYNWsWU6ZMYdq0aQCsXr2aFStW0KJFC2688Ub69evHxIkT+cMf/sDMmTMBOOuss5g2bRo33ngjpaWl7Nu3j88//5wVK1bQv39/AO655x5yc3M5ePAg55xzDm+99Rbdu3cHQr/ftGLFCrZu3coll1zC4sWLadmyJffddx8PPPAAEydOrHIuV1xxBQ8//DADBgxg4sSJTJ48menTpzNmzBhatWrFD3/4QwBeeuklli5dSl5eHkOHDuXRRx+lqKiI3bt3k5OTQ0lJCevXr+eNN97A3SkuLmb58uXce++9/P3vf6cs7P7YwsLCKu8j2bRpE506dap8n5+fz6ZNmyJu+9xzz7F8+XKOP/54HnzwQTp16lRj+W3btnHkkUfStGnTOvdbH0qwREREJCYN7XVTj1t8aDbM1GrRokWVBGHOnDmUlpYCod/pGjFiBBUVFezfv7/K7ygVFxfTokULAJYvX87zzz8PwAUXXEDbtm0BOO2001i9ejW7du2iefPm9OzZk9LSUl599VVmzJgBwDPPPMPMmTM5cOAAFRUVrFmzpjLBGjFiBACvv/46a9asoaioCID9+/fTt2/fKufxySefsHPnTgYMGACEenOGDx9e5/kXFRUxbtw4Lr/8ci655BLy8/MpKSmhpKSEHj16ALB7927Wr1/PMcccc1j5upIroLJHL1ykmf2GDh3KqFGjaN68OY8//jijR49myZIlNZaPdr/1pQRLREQy009+kuoaiEiy1dbj9OUv174+Ly+qHqv6uOGGGxg3bhzFxcUsW7aMSZMmVa5r2bJllW0jfbBv1qwZBQUFzJ49mzPPPJPu3buzdOlSNmzYQLdu3di4cSP3338/q1atom3btlx55ZXs3bv3sGO4O4MGDeLpp5+O6/kBTJgwgQsuuICFCxfSp08fFi9ejLtzyy23cN1111XZ9v33349qnytXrqwse9ddd5Gfn8+ysH+b8vJyBkborWzXrl3l62uuuYbx48cD1Fg+Ly+PnTt3cuDAAZo2bUp5eTlf/epXozvxWijBEhGRzHTmmamuQaMSy71f6aix9erUFf90rLPU7ZNPPqFjx44ANc58B6GhgPPnz+f2229n0aJFVSZwOOuss7j//vt58sknOeWUUxg3bhynnXYaZsann35Ky5YtadOm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f6rhmIurlwlcRujerD0R1kWq3yp3H0ZoCOULhHr5IBSfe6rF58vu/vShotHsX0REEk8JlohIlnD3T4CJwKNmdpGZfdnMmpnZEDOL5r6dloQ+yG8BMLP/JtSDFQ/zgKFmNtjMmphZjpkNNLP8ugqa2eVmdoyFHAvcA7zSkEq4uxO6/+t2C01n38LMvgLMItRT93ANRScDZ5rZPWaWa2atzewG4ApgfDTHNrMvBefSxt0/Bz4lNGEHhBLbMWbWOzjPlmZ2gZm1DtZvJjSDooiIpJgSLBGRLOLuDwDjgNsJJUofEOqReiGKsmsIzbD3GqEP9KcQmpo8HvX6gNBU8beG1etmovs7dSLwF2B3UJ93CN2z1NC6/Br4NnAToWGVFcDpwAB3j3SfFe6+ntCQxlMJ3f9VQajHa7C71ydG3wbeN7NPgTGE7k3D3UsJndMjhIYbvgtcGVbup4SSwp3hMw+KiEjyWejLOhEREYnEzM4DngbOcfeyFFdHRETSnBIsERGROpjZUKCju0eaGENERKSSEiwREREREZE40T1YIiIiIiIicaIES0REREREJE7q+rHJpMjLy/OCgoJUV0NERERERCQqq1ev3uru7asvT4sEq6CggNLS0lRXQ0REMsk774SeTzghtfUQEZGMZGb/jLQ8LRIsERGRuLvuutDzsmUprYaIiGQXJVgiIiLS6Fw9Z1Wt65+48vQk1UREpCpNciEiIiIiIhIn6sESERGRjFNbD5d6tzLH559/Tnl5OXv37k11VSSD5eTkkJ+fT7NmzaLaXgmWiIiIiDRK5eXltG7dmoKCAsws1dWRDOTubNu2jfLycjp37hxVGSVYIiKSmW6/PdU1EJEE27t3r5IrSSgzo127dmzZsiXqMkqwREQkM517bqprICJJoORKEq2+15gmuRARkcxUVhZ6iIiIJJESLBERyUw/+EHoISKSQK1ataryfs6cOYwdOzamfRYUFLB169aY9hFP69ato7CwkB49erBhwwZmzJhBt27duPzyy+u1n507d/LYY481qA5z586la9eudO3alblz50bcZs6cObRv357CwkIKCwuZNWtWneU3btxI79696dq1KyNGjGD//v0Nql84DREUERERzbonkiIHDhygadPEfiQ/ePAgTZo0aXD5F154gWHDhjF58mQAHnvsMRYtWhT1pA+HHEqwvvvd79ar3Pbt25k8eTKlpaWYGaeddhrFxcW0bdv2sG1HjBjBI488EnX58ePHc9NNNzFy5EjGjBnDE088wfXXX1+v+lWnHiwRERERkQR48cUX6d27Nz169ODcc89l8+bNAEyaNIlrr72W8847jyuuuIJt27Zx3nnn0aNHD6677jrcHYApU6YwY8YMAG666Sa+/vWvA/DKK6/wrW99C4Drr7+eXr16cdJJJ3HnnXdWHrugoIC77rqLfv368Zvf/IaSkhL69u1Lz549GT58OLt37z6svmVlZfTp04fu3btz8cUXs2PHDhYuXMj06dOZNWsWZ599NmPGjOG9996juLiYBx98kD/96U+VPUY9evRg165dAEydOpXTTz+d7t27V9ZrwoQJbNiwgcLCQm6++eao4/jHP/6RQYMGkZubS9u2bRk0aBAvvfRSzOXdnSVLlnDppZcCMHr0aF544YWo91sT9WCJiIg0ErX1MoF6mkQYOPDwZZddBt/9LuzZA9/4xuHrr7wy9Ni6FYIP2pWWLavzkJ999hmFhYWV77dv305xcTEA/fr14/XXX8fMmDVrFlOmTGHatGkArF69mhUrVtCiRQtuvPFG+vXrx8SJE/nDH/7AzJkzATjrrLOYNm0aN954I6Wlpezbt4/PP/+cFStW0L9/fwDuuececnNzOXjwIOeccw5vvfUW3bt3B0K/37RixQq2bt3KJZdcwuLFi2nZsiX33XcfDzzwABMnTqxyLldccQUPP/wwAwYMYOLEiUyePJnp06czZswYWrVqxQ9/+EMAXnrpJZYuXUpeXh5Dhw7l0UcfpaioiN27d5OTk0NJSQnr16/njTfewN0pLi5m+fLl3Hvvvfz973+nLOz+2MLCwirvI9m0aROdOnWqfJ+fn8+mTZsibvvcc8+xfPlyjj/+eB588EE6depUY/lt27Zx5JFHVvYg1rbf+lCCJSIiIiLSQC1atKiSIMyZM4fS0lIg9DtdI0aMoKKigv3791cZUldcXEyLFi0AWL58Oc8//zwAF1xwQeXQt9NOO43Vq1eza9cumjdvTs+ePSktLeXVV1+t7Nl65plnmDlzJgcOHKCiooI1a9ZUJlgjRowA4PXXX2fNmjUUFRUBsH//fvr27VvlPD755BN27tzJgAEDgFBvzvDhw+s8/6KiIsaNG8fll1/OJZdcQn5+PiUlJZSUlNCjRw8Adu/ezfr16znmmGMOK19XcgVU9uiFizSz39ChQxk1ahTNmzfn8ccfZ/To0SxZsqTG8tHut76UYImISGb6yU9SXQNB93aFUyySoLYepy9/ufb1eXlR9VjVxw033MC4ceMoLi5m2bJlTJo0qXJdy5Ytq2wb6YN9s2bNKCgoYPbs2Zx55pl0796dpUuXsmHDBrp168bGjRu5//77WbVqFW3btuXKK69k7969hx3D3Rk0aBBPP/10XM8PQsP+LrjgAhYuXEifPn1YvHgx7s4tt9zCddddV2Xb999/P6p9rly5srLsXXfdRX5+PsvC/m3Ky8sZGKG3sl27dpWvr7nmGsaPHw9QY/m8vDx27txZeR9ceXk5X/3qV6M78VroHiwREclMZ54ZeoiIpMgnn3xCx44dAWqc+Q5CQwHnz58PwKJFi9ixY0eVdffffz9nnXUW/fv35/HHH6ewsBAz49NPP6Vly5a0adOGzZs3s2jRooj779OnD3/+85959913AdizZw//+Mc/qmzTpk0b2rZty6uvvgrAU089VdmbVZsNGzZwyimnMH78eHr16sW6desYPHgwTz75ZOV9Xps2beLjjz+mdevWlfdo1aZ3796UlZVRVlZGcXExgwcPpqSkhB07drBjxw5KSkoYPHjwYeUqKioqXy9YsIBu3boB1FjezDj77LN59tlngdC/0bBhw+qsX13UgyUiIpnpL38JPSvJAuq+f0tipxhLdZMmTWL48OF07NiRPn36sHHjxojb3XnnnYwaNYqePXsyYMCAKkPp+vfvzz333EPfvn1p2bIlOTk5lfdfnXrqqfTo0YOTTjqJ4447rnIIYHXt27dnzpw5jBo1in379gFw9913c/zxx1fZbu7cuYwZM4Y9e/Zw3HHHMXv27DrPcfr06SxdupQmTZpw4oknMmTIEJo3b87atWsrhyG2atWKefPm0aVLF4qKijj55JMZMmQIU6dOjeoerNzcXO644w5OPz3U0ztx4kRyc3MrX/fq1Yvi4mJmzJjBggULaNq0Kbm5ucyZM6fO8vfddx8jR47k9ttvp0ePHlx99dV1nnNdLNLYw2Tr1auXHxqrKiIiEheHho/EechPKsUyyUUsH/5jGb6WqGFxqTqf2qRjnTLd2rVrK3spRBIp0rVmZqvdvVf1bdWDJSIiEmepmu1PPSgiIqmne7BERERERETipME9WGbW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8RLgWTNrR91x0CQZIiJpSD1YIiJZyN0/ASYCjwa/zfRlM2tmZkPMbEoUu2hJ6AP+FgAz+29CPVjxMA8YamaDzayJmeWY2UAzy6+j3CfAVwkNNSzk30PpTgNWxqlu4R4klNQ9YWZfCeo5CrgNuDnaWQLN7FvBfVVfADuDxQepOw6bCU2gISIiaUQJlohIlnL3B4BxwO2EEqUPgLHAC1GUXQNMI9QTthk4BfhznOr1AaGp4m8Nq9fN1PE3y0M+OvQIygJsdvf98ahbteNtIzRkLwdYQ2jY5Tjg28E9adE6H3jbzHYTmvBipLvvjSIODwGXBrM0zojHOYmISOxMP8MhIiIiIiISH+rBEhERERERiRMlWCIiIiIiInGiBEtERERERCROlGCJiIiIiIjESVr8DlZeXp4XFBSkuhoiIiIiIiJRWb169VZ3b199eVokWAUFBZSWlqa6GiIikkneeSf0fMIJqa2HiIhkJDP7Z6TlaZFgiYiIxN1114Wely1LaTVERCS7KMESERERCVw9Z1Wt65+48vQk1UREGitNciEiIiIiIhIn6sESERGJs0zrBcm084lFbbHIpjiki88//5zy8nL27t2b6qpIBsvJySE/P59mzZpFtb0SLBERERFplMrLy2ndujUFBQWYWaqrIxnI3dm2bRvl5eV07tw5qjJKsEREJDPdfnudm6g3QqRx27t3r5IrSSgzo127dmzZsiXqMkqwREQkM517bqprICJJoORKEq2+15gmuRARkcxUVhZ6iIiIJJESLBERyUw/+EHoISKSQK1ataryfs6cOYwdOzamfRYUFLB169aY9hFP69ato7CwkB49erBhwwZmzJhBt27duPzyy+u1n507d/LYY481qA5z586la9eudO3alblz50bcZs6cObRv357CwkIKCwuZNWtWneU3btxI79696dq1KyNGjGD//v0Nql84JVgiIiIiIily4MCBhB/j4MGDMZV/4YUXGDZsGH/961/p0qULjz32GAsXLmT+/Pn12k9DE6zt27czefJkVq5cyRtvvMHkyZPZsWNHxG1HjBhBWVkZZWVlfOc736mz/Pjx47nppptYv349bdu25Yknnqh3/arTPVgiIiJppK4p0WujiTlE0suLL77I3Xffzf79+2nXrh3z58/n6KOPZtKkSXz44Ye8//775OXl8fDDDzNq1Ci2bNnCGWecgbsDMGXKFHJycrjxxhu56aabePPNN1myZAmvvPIKs2fPZt68eVx//fWsWrWKzz77jEsvvZTJkycDoV6wq666ipKSEsaOHUtubi533nkn+/bto0uXLsyePfuw3reysjLGjBnDnj176NKlC08++SSvvfYa06dPp0mTJixfvpwTTjiB9957j+LiYq666ip69uzJ97//fSB0r9Ly5ctp3bo1U6dO5ZlnnmHfvn1cfPHFTJ48mQkTJrBhwwYKCwsZNGgQU6dOjSqOf/zjHxk0aBC5ubkADBo0iJdeeolRo0bFVH7kyJEsWbKEX/7ylwCMHj2aSZMmcf3110e135oowRIRERGRzDBw4OHLLrsMvvtd2LMHvvGNw9dfeWXosXUrXHpp1XXLltV5yM8++4zCwsLK99u3b6e4uBiAfv368frrr2NmzJo1iylTpjBt2jQAVq9ezYoVK2jRogU33ngj/fr1Y+LEifzhD39g5syZAJx11llMmzaNG2+8kdLSUvbt28fnn3/OihUr6N+/PwD33HMPubm5HDx4kHPOOYe33nqL7t27A6Hfb1qxYgVbt27lkksuYfHixbRs2ZL77ruPBx54gIkTJ1Y5lyuuuIKHH36YAQMGMHHiRCZPnsz06dMZM2YMrVq14oc//CEAL730EkuXLiUvL4+hQ4fy6KOPUlRUxO7du8nJyaGkpIT169fzxhtv4O4UFxezfPly7r33Xv7+979TFnZ/bGFhYZX3kWzatIlOnTpVvs/Pz2fTpk0Rt33uuedYvnw5xx9/PA8++CCdOnWqsfy2bds48sgjadq0aZ37rQ8lWCIiIhKThva6qcctPvRzA6nVokWLKgnCnDlzKC0tBUK/0zVixAgqKirYv39/ld9RKi4upkWLFgAsX76c559/HoALLriAtm3bAnDaaaexevVqdu3aRfPmzenZsyelpaW8+uqrzJgxA4BnnnmGmTNncuDAASoqKlizZk1lgjVixAgAXn/9ddasWUNRUREA+/fvp2/fvlXO45NPPmHnzp0MGDAACPXmDB8+vM7zLyoqYty4cVx++eVccskl5OfnU1JSQklJCT169ABg9+7drF+/nmOOOeaw8nUlV0Blj164SDP7DR06lFGjRtG8eXMef/xxRo8ezZIlS2osH+1+60sJloiIZKaf/CTVNRCRZKutx+nLX659fV5eVD1W9XHDDTcwbtw4iouLWbZsGZMmTapc17JlyyrbRvpg36xZMwoKCpg9ezZnnnkm3bt3Z+nSpWzYsIFu3bqxceNG7r//flatWkXbtm258sor2bt372HHcHcGDRrE008/HdfzA5gwYQIXXHABCxcupE+fPixevBh355ZbbuG6666rsu37778f1T5XrlxZWfauu+4iPz+fZWH/NuXl5QyM0FvZrl27ytfXXHMN48ePB6ixfF5eHjt37uTAgQM0bdqU8vJyvvrVr0Z34rVQgiUiIpnpzDNTXYNGJZZ7v9JRY+vVqSv+6Vhnqdsnn3xCx44dAWqc+Q5CQwHnz5/P7bffzqJFi6pM4HDWWWdx//338+STT3LKKacwbtw4TjvtNMyMTz/9lJYtW9KmTRs2b97MokWLIiYeffr04Xvf+x7vvvsuX/va19izZw/l5eUcf/zxldu0adOGtm3b8uqrr9K/f3+eeuqpyt6s2mzYsIFTTjmFU045hddee41169YxePBg7rjjDi6//HJatWrFpk2baNasGa1bt2bXrl117rN3795Vera2b9/OrbfeWhmXkpISfvrTnx5WrqKigg4dOgCwYMECunXrBsDgwYMjljczzj77bJ599llGjhzJ3LlzGTZsWJ31q0uDEywz6wT8AvgK8AUw090fMrNc4NdAAfA+cJm7R57mQ0REJFH+8pfQsxItEUmRSZMmMXz4cDp27EifPn3YuHFjxO3uvPNORo0aRc+ePRkwYECVoXT9+/fnnnvuoW/fvrRs2ZKcnJzK+69OPfVUevTowUknncRxxx1XOQSwuvbt2zNnzhxGjRrFvn37ALj77rurJFgQSgIPTXJx3HHHMXv27DrPcfr06SxdupQmTZpw4oknMmTIEJo3b87atWsrhyG2atWKefPm0aVLF4qKijj55JMZMmQIU6dOjeoerNzcXO644w5OPz30RcPEiRMrJ6yYOHEivXr1ori4mBkzZrBgwQKaNm1Kbm4uc+bMqbP8fffdx8iRI7n99tvp0aMHV199dZ3nXBeLNPYwqoJmHYAO7v6/ZtYaWA1cBFwJbHf3e81sAtDW3cfXtq9evXr5obGqIiIicXHoW9xahvwkqpcjlt6IVM0imIoerET2yjT03zZd49/QOmd6z9fatWsreylEEinStWZmq929V/VtG/w7WO5e4e7/G7zeBawFOgLDgEN9oHMJJV0iIiIiIiIZLy73YJlZAdADWAkc7e4VEErCzOyoGspcC1wLRJxRREREJFbrPtrF1Ay7tyiT6L4jEclEDe7BOsTMWgHPAT9w90+jLefuM929l7v3at++fazVEBEREZEs1NDbXUSiVd9rLKYeLDNrRii5mu/uzweLN5tZh6D3qgPwcSzHEBERScd7TNJx1r10jJP8WzpeM4mSrN7JnJwctm3bRrt27eLy+0Ui1bk727ZtIycnJ+oyscwiaMATwFp3fyBs1QJgNHBv8Py7hh5DRESkwaZP51cL3k51LUQkgfLz8ykvL2fLli2propksJycHPLz86PePpYerCLg28DfzKwsWHYrocTqGTO7GvgXUPdPQIuIiMRbYSEflH2e6lqISAI1a9aMzp07p7oaIlU0OMFy9xVATX2x5zR0vyIiInGxeDHd3n6HtSedkeqaSANpyKOINEYxT3IhIiKSlu6+mwtffDLVtRARkSwTl2naRURERBqLRE02kU2TWIhIzdSDJSIiIiIiEifqwRIRkaRIdq/BzR/tSsh+G6tMOx8RkXSlHiwREREREZE4UQ+WiIhkpF+MviXVVaiRepOkvtLxmknFvWyaPVIaAyVYIiKSkTZ3ODbVVRARkSykBEtERKpo6LfS6fbN8qllrwLwZmH/FNdERNJBXW1burVh0ngpwRIRkYx03kvzASVYIiKSXEqwRERSJJHfpuoeBhHJRul4r5pkH80iKCIiIiIiEifqwRKRrKCx9yIiIpIM6sESERERERGJk5h6sMzsSeBC4GN3PzlYlgv8GigA3gcuc/cdsVVTRESkfmZdMznVVRARkSwU6xDBOcAjwC/Clk0AXnH3e81sQvB+fIzHERHJOpqoIjY72h2d6ipIAmkyg+ik6/DoTPr3S9cYS+rENETQ3ZcD26stHgbMDV7PBS6K5RgiIiINcfrKlzl95cuproaIiGSZRExycbS7VwC4e4WZHRVpIzO7FrgW4JhjjklANUREkkM9TSHp9o30wKXPAbCq96AU10RERLJJyia5cPeZ7t7L3Xu1b98+VdUQERERERGJm0T0YG02sw5B71UH4OMEHENEJGnSrWdGRCQZ1PaJNEwierAWAKOD16OB3yXgGCIiIiIiImkn1mnanwYGAnlmVg7cCdwLPGNmVwP/AobHWkkRkcYqUd8A65tlEZH40v20Ei8xJVjuPqqGVefEsl8REZFY/ex796a6CiIikoUScQ+WiEiD6RvExMuW3q/drY9MdRVE0l62tAexiiVO+ruWfVI2i6CIiEgiFa34PUUrfp/qaoiISJZRD5aIZAx9EyvhzgySqz/3uzDFNRGReMm2dl69X42TerBERERERETiRD1YIlkslm8Ca/vmTDPniYiI1E1/1zKTerBERERERETiRAmWiIiIiIhInGiIoEgjoJtcE0/DNDLPQzdNT3UVREQSpq6/W/p8kDpKsEREJCPtb56T6iqIiEgWUoIl0sjpGyyRyM5e8iwAS79+aYprIiKSXhI1yVUsx82kzyu6B0tERDJSrzcW0+uNxamuhoiIZBn1YIlkOE2ZLiIiIpI8CevBMrPzzewdM3vXzCYk6jgiIiIiIiLpIiE9WGbWBHgUGASUA6vMbIG7r0nE8bJJKsauxnKPT6Lqm8j7jhraM1PXMbNl3LGIiIikXmMbwZJJn5MS1YN1BvCuu7/n7vuBXwHDEnQsERERERGRtGDuHv+dml0KnO/u3wnefxvo7e5jw7a5Frg2eHsC8E7cKxKbPGBrqiuRZRTz5FPMk08xTy7FO/kU8+RTzJNPMU+udI33se7evvrCRE1yYRGWVcnk3H0mMDNBx4+ZmZW6e69U1yObKObJp5gnn2KeXIp38inmyaeYJ59inlyNLd6JGiJYDnQKe58PfJigY4mIiIiIiKSFRCVYq4CuZtbZzL4EjAQWJOhYIiIiIiIiaSEhQwTd/YCZjQX+CDQBnnT3txNxrARK2+GLGUwxTz7FPPkU8+RSvJNPMU8+xTz5FPPkalTxTsgkFyIiIiIiItkoYT80LCIiIiIikm2UYImIiIiIiMRJ1iVYZpZrZi+b2frguW0N2x1pZs+a2TozW2tmfesqb2a3mNm7ZvaOmQ1O1jmlu3rE/H0z+5uZlZlZadjyXwfLyoJtyoLlBWb2Wdi6x5N0SmktDvGeZGabwuL6jbB1usYjiEPMpwZtzVtm9lszOzJYrmu8BnGIudryeoo25sG2Tczsr2b2+7BlasvrKQ4xV3teD3GIt9ryeopDzNOyLc+6BAuYALzi7l2BV4L3kTwEvOTu/wmcCqytrbyZnUhotsSTgPOBx8ysScLOonGJNuYAZ7t7YfhvHbj7iGBZIfAc8HzY9hsOrXP3MYmofCMUU7wDD4bFdSHoGq9DrDF/GTjZ3bsD/wBuCVunazyyWGOutrz+6hPz7/Pvv5uA2vIGiinmAbXn0Ys13mrL6y/WmKdlW56NCdYwYG7wei5wUfUNzOwI4CzgCQB33+/uO+soPwz4lbvvc/eNwLvAGfGvfqNUZ8yjYWYGXAY8HZ9qZay4xLuG/eoajyymmLt7ibsfCN6+Tui3A6V2sV7nasvrL6qYm1k+cAEwq4b1asujF5eY17BfXeeHiyneassbJNZrPC3b8mxMsI529wqA4PmoCNscB2wBZgddkbPMrGUd5TsCH4TtozxYJtHFHMCBEjNbbWbXRljfH9js7uvDlnUO/o3+ZGb941vtRise8R4bDHF4Mqy7Xdd4zeJ1jQNcBSwKe69rPLJYY662vP6ijfl04EfAFzWsV1sevXjEXO159OJ1jYPa8mjFGvO0bMsT8jtYqWZmi4GvRFh1W5S7aAr0BG5w95Vm9hChLsc7ajtshGVZMwd+HGIOUOTuH5rZUcDLZrbO3ZeHrR9F1W88K4Bj3H2bmZ0GvGBmJ7n7p/U+gUYmwfH+GfBjQtfvj4FphP5Q6BpP8DVuZrcBB4D5waKsvcYhae3KYYeNsEzXeZQxN7MLgY/dfbWZDaxhM7XlYRIcc7Xn1STjGldbXlWS2pXDikVYlrRrPCMTLHc/t6Z1ZrbZzDq4e4WZdQA+jrBZOVDu7iuD98/y7zGhNZUvBzqF7SMf+DCmE2lE4hBz3P3D4PljM/stoa7c5cE+mgKXAKeFbb8P2Be8Xm1mG4DjgVIyXCLj7e6bw/b1c+DQzaS6xmsQp2t8NHAhcI576AcKs/kah4THXG15BHGIeRFQbKHJFHKAI8xsnrt/K9iH2vJqEhlzteeH+//s3X14VNW59/HvfRAJAgVC0KJRg4oWUUwwCAgoVhDfCNVKgWrFU1vFltriU49YFaHVHt9Fqh5LUaFqba1aRAs2oiBgVQgaUQGFiK3BiBBBocir9/PH7MQhTJJJ5i2Z+X2ua66Z2Xutvde+2ezMPWvtNUk4x3UtryHBMW+S1/JMHCI4GxgTvB4DPFOzgLt/AnxkZscEi04HVtRTfzYwysxamVlXoBuwJP7Nb5bqjbmZtTGzdlWvgTOAd8KKDAZWuXt5WJ3OFtywaGZHEIr5Bwk5guYlpngHF6gq5/H1v4PO8drFGvMzgWuAInffFlZH53jtYr2u6FrecNH8/bzW3XPdPY/QDeYvVX3wDOha3jAxxVzX8waLNd66ljdcrNeVpnktd/eMegCdCM0ysjp4zg6WHwzMCSuXT+ibheXALKBjXfWDddcBZcB7wFmpPtam8ogm5oTue3sreLwLXFdjGzOAsTWWfTco+xbwBjAs1cfaFB6xxht4BHg7OPdnA13C1ukcT0zM1xAaK14aPB4IluscT1zMdS1PQMxrlB8EPFdjma7lSYy5rudJj7eu5cmPeZO8llvQABEREREREYlRJg4RFBERERERSQglWCIiIiIiInGiBEtERERERCROlGCJiIiIiIjEiRIsERERERGROFGCJSIiIiIiEidKsEREREREROJECZaIiIiIiEicKMESERERERGJEyVYIiIiIiIicaIES0REREREJE6UYImIiIiIiMSJEiwREalmZpPM7NFUt6MmM1tgZj9KdTtiZWbnmdlHZrbVzArM7Bgze9PMtpjZlalun4iIxE4JlohIhjGz75tZSfAhv8LM5prZgFS3Kx7M7CUzczPbL8btnGtmS8zsP2ZWaWaPmtkh9dQ51sxmm9nnQcI038xOrlHsDmCcu7d19zeB/wEWuHs7d58aQ3s/NLPBja0vIiLxowRLRCSDmNlVwBTgt8BBwGHA/cDwFDYrLszsQiCmxCrYzgXAn4B7gBygB7ATWGRmHWqpcyTwCvA20BU4GPgbUGxm/cKKHg68W8d7ERFp5pRgiYhkCDNrD/wa+Km7P+3u/3H3Xe7+rLtfHVZ0fzP7Y9AL866ZFYZtY4KZlQXrVpjZeWHrLjGzxWZ2h5ltMrO1ZnZW2PoFZvYbM3slqF9sZjlh6/ua2T/NbLOZvWVmgxp4bDcS6hFqNDMz4E7gJnd/zN2/dPdPgB8B24Cf11J1EvCqu1/n7p+5+5agR+oR4FYza2VmW4EWwFtBDF8CTgPuDXoTjzazs4O4bjGzdWb2y7C2nWtmpUF8/mlmPYPljxBKlJ8NthNTDEREJDZKsEREMkc/IItQz0pdioA/Ax2A2cC9YevKgIFAe2Ay8KiZdQlb3wd4j1DPz23Ag0HSUuX7wH8DBwL7A78ECIbf/R24CcgOlj9lZp2jPLbfAv8HfBJl+docQyhZ+Wv4Qnf/CngKOKOWekNq1gk8AfQHWrh722DZCe5+pLt/G1jE10MG3wceBC5393bAccBLAGbWC3gIuBzoBPwemG1mrdz9B8C/gWHBdm5r5LGLiEgcKMESEckcnYCN7r67nnKL3X2Ou+8h1ANzQtUKd/+ru3/s7l+5+1+A1cBJYXX/5e5/COrOBLoQGopY5WF3f9/dvySUfOQHyy8C5gT7/crdXwBKgLPrO6igh60/8Lv6ykahqketIsK6CqC2hC+njjr/BXSMcv+7gGPN7Bvuvsnd3wiW/xj4vbu/7u573H0msAPoG+V2RUQkSZRgiYhkjkogJ4oJIMJ7gbYBWVV1zOzisGFqmwn1suREquvu24KXbSOtD7Zdte5wYETVdoNtDyCUoNXKzP6L0D1kP48iccTMfhUMo9tqZg9EKLIxeI603y7Ahlo2vbGOOl8Bm+prW+C7hJLKf5nZy2H3bx0O/L8a8TmU0L1eIiLShCjBEhHJHK8C24HvNKaymR0O/AEYB3Ry9w7AO4DVVS9KHwGPuHuHsEcbd7+lnnrfAAqBv5jZJ8DSYHm5mQ2sWdjdfxsMo2vr7mMjbO89oBwYEb4wSOS+C7xcSzvm1awT+B6he7O2RVi3D3df6u7DCQ2hnEWolw9C8bm5RnwOcPfHq6pGs30REUk8JVgiIhnC3T8HJgL3mdl3zOwAM2tpZmeZWTT37bQh9EF+A4CZ/TehHqx4eBQYZmZDzayFmWWZ2SAzy62n3ueEenHyg0fVkMITgdcb2gh3d0L3f10fTGff2sy+CUwn1FNX2zDEycDJZnazmWWbWTsz+xlwMXBNNPs2s/3N7EIza+/uu4AvgD3B6j8AY82sj4W0MbNzzKxdsH49cERDj1dEROJPCZaISAZx97uAq4DrCSVKHxHqkZoVRd0VhGbYe5XQB/rjCU1NHo92fURoqvhfhbXraur5O+Uhn1Q9+HoI33p339nItvwF+AEwntCwygqgN3Cqu0e6zwp3X01oSOMJwIdBne8CQ929ITH6AfChmX0BjCV0bxruXkLoPqx7CQ03XANcElbvfwklhZvDZx4UEZHks9CXdSIiIhKJmZ0BPA6c7u6lKW6OiIg0cUqwRERE6mFmw4BD3D3SxBgiIiLVlGCJiIiIiIjEie7BEhERERERiRMlWCIiIiIiInFS349NJkVOTo7n5eWluhkiIiIiIiJRWbZs2UZ371xzeZNIsPLy8igpKUl1M0REJJ28917o+ZhjUtsOERFJS2b2r0jLm0SCJSIiEneXXx56XrAgpc0QEZHMogRLREREmp1LZyytc/2Dl/ROUktERPamSS5ERERERETiRD1YIiIiknbq6uFS71b62LVrF+Xl5Wzfvj3VTZE0lpWVRW5uLi1btoyqvBIsEREREWmWysvLadeuHXl5eZhZqpsjacjdqayspLy8nK5du0ZVRwmWiIikp+uvT3ULRCTBtm/fruRKEsrM6NSpExs2bIi6jhIsERFJT4MHp7oFIpIESq4k0Rp6jmmSCxERSU+lpaGHiIhIEinBEhGR9PSLX4QeIiIJ1LZt273ez5gxg3HjxsW0zby8PDZu3BjTNuJp1apV5OfnU1BQQFlZGVOnTqV79+5ceOGFDdrO5s2buf/++xvVhpkzZ9KtWze6devGzJkzI5aZMWMGnTt3Jj8/n/z8fKZPn15v/bVr19KnTx+6devGyJEj2blzZ6PaF05DBEVERESz7omkyO7du9lvv8R+JN+zZw8tWrRodP1Zs2YxfPhwJk+eDMD999/P3Llzo570oUpVgvWTn/ykQfU+++wzJk+eTElJCWbGiSeeSFFRER07dtyn7MiRI7n33nujrn/NNdcwfvx4Ro0axdixY3nwwQe54oorGtS+mtSDJSIiIiKSAM8++yx9+vShoKCAwYMHs379egAmTZrEZZddxhlnnMHFF19MZWUlZ5xxBgUFBVx++eW4OwC33XYbU6dOBWD8+PF8+9vfBuDFF1/koosuAuCKK66gsLCQHj16cOONN1bvOy8vj1//+tcMGDCAv/71rxQXF9OvXz969erFiBEj2Lp16z7tLS0tpW/fvvTs2ZPzzjuPTZs2MWfOHKZMmcL06dM57bTTGDt2LB988AFFRUXcfffdvPzyy9U9RgUFBWzZsgWA22+/nd69e9OzZ8/qdk2YMIGysjLy8/O5+uqro47jP/7xD4YMGUJ2djYdO3ZkyJAhPP/88zHXd3deeuklLrjgAgDGjBnDrFmzot5ubdSDJSIi0kzU1csE6mkSYdCgfZd973vwk5/Atm1w9tn7rr/kktBj40YIPmhXW7Cg3l1++eWX5OfnV7//7LPPKCoqAmDAgAG89tprmBnTp0/ntttu48477wRg2bJlLF68mNatW3PllVcyYMAAJk6cyN///nemTZsGwCmnnMKdd97JlVdeSUlJCTt27GDXrl0sXryYgQMHAnDzzTeTnZ3Nnj17OP3001m+fDk9e/YEQr/ftHjxYjZu3Mj555/PvHnzaNOmDbfeeit33XUXEydO3OtYLr74Yn73u99x6qmnMnHiRCZPnsyUKVMYO3Ysbdu25Ze//CUAzz//PPPnzycnJ4dhw4Zx33330b9/f7Zu3UpWVhbFxcWsXr2aJUuW4O4UFRWxcOFCbrnlFt555x1Kw+6Pzc/P3+t9JOvWrePQQw+tfp+bm8u6desiln3qqadYuHAhRx99NHfffTeHHnporfUrKyvp0KFDdQ9iXdttCCVYIiIiIiKN1Lp1670ShBkzZlBSUgKEfqdr5MiRVFRUsHPnzr2G1BUVFdG6dWsAFi5cyNNPPw3AOeecUz307cQTT2TZsmVs2bKFVq1a0atXL0pKSli0aFF1z9YTTzzBtGnT2L17NxUVFaxYsaI6wRo5ciQAr732GitWrKB///4A7Ny5k379+u11HJ9//jmbN2/m1FNPBUK9OSNGjKj3+Pv3789VV13FhRdeyPnnn09ubi7FxcUUFxdTUFAAwNatW1m9ejWHHXbYPvXrS66A6h69cJFm9hs2bBijR4+mVatWPPDAA4wZM4aXXnqp1vrRbrehlGCJiEh6+u1vU90CQfd2hVMskqCuHqcDDqh7fU5OVD1WDfGzn/2Mq666iqKiIhYsWMCkSZOq17Vp02avspE+2Lds2ZK8vDwefvhhTj75ZHr27Mn8+fMpKyuje/furF27ljvuuIOlS5fSsWNHLrnkErZv377PPtydIUOG8Pjjj8f1+CA07O+cc85hzpw59O3bl3nz5uHuXHvttVx++eV7lf3www+j2ubrr79eXffXv/41ubm5LAj7tykvL2dQhN7KTp06Vb/+8Y9/zDXXXANQa/2cnBw2b95cfR9ceXk5Bx98cHQHXgfdgyUiIunp5JNDDxGRFPn888855JBDAGqd+Q5CQwEfe+wxAObOncumTZv2WnfHHXdwyimnMHDgQB544AHy8/MxM7744gvatGlD+/btWb9+PXPnzo24/b59+/LKK6+wZs0aALZt28b777+/V5n27dvTsWNHFi1aBMAjjzxS3ZtVl7KyMo4//niuueYaCgsLWbVqFUOHDuWhhx6qvs9r3bp1fPrpp7Rr1676Hq269OnTh9LSUkpLSykqKmLo0KEUFxezadMmNm3aRHFxMUOHDt2nXkVFRfXr2bNn0717d4Ba65sZp512Gk8++SQQ+jcaPnx4ve2rj3qwREQkPf3zn6FnJVlA/fdvSewUY6lp0qRJjBgxgkMOOYS+ffuydu3aiOVuvPFGRo8eTa9evTj11FP3Gko3cOBAbr75Zvr160ebNm3Iysqqvv/qhBNOoKCggB49enDEEUdUDwGsqXPnzsyYMYPRo0ezY8cOAG666SaOPvrovcrNnDmTsWPHsm3bNo444ggefvjheo9xypQpzJ8/nxYtWnDsscdy1lln0apVK1auXFk9DLFt27Y8+uijHHnkkfTv35/jjjuOs846i9tvvz2qe7Cys7O54YYb6N071NM7ceJEsrOzq18XFhZSVFTE1KlTmT17Nvvttx/Z2dnMmDGj3vq33noro0aN4vrrr6egoIBLL7203mOuj0Uae5hshYWFXjVWVUREJC6qho/EechPKsUyyUUsH/5jGb6WqGFxqTqeujTFNqW7lStXVvdSiCRSpHPNzJa5e2HNsurBEhERibNUzfanHhQRkdRTgiUiIk2eJgdIrXSbHl49TSKSSI2e5MLMDjWz+Wa20szeNbOfB8uzzewFM1sdPO/7E8siIiIiInHQFG53kfTW0HMslh6s3cD/c/c3zKwdsMzMXgAuAV5091vMbAIwAbgmhv2IiIhIBtKQR6lPVlYWlZWVdOrUKS6/XyRSk7tTWVlJVlZW1HUanWC5ewVQEbzeYmYrgUOA4cCgoNhMYAFKsEREJNmmTEl1C0QkwXJzcykvL2fDhg2pboqksaysLHJzc6MuH5d7sMwsDygAXgcOCpIv3L3CzA6spc5lwGVAxF91FhERiUl+fqpbICIJ1rJlS7p27ZrqZojsJeYfGjaztsBTwC/c/Yto67n7NHcvdPfCzp07x9oMERGRvc2bF3qIiIgkUUw9WGbWklBy9Zi7Px0sXm9mXYLeqy7Ap7E2UkREpMFuuin0PHhwatsRge4tClEcRCQdxTKLoAEPAivd/a6wVbOBMcHrMcAzjW+eiIiIiIhI8xFLD1Z/4AfA22ZWGiz7FXAL8ISZXQr8GxgRUwtFRESkSVNPVNOVbr9hJtIcxDKL4GKgtvkwT2/sdkVERERERJqruMwiKCIiIpIJEtVbF0tPk3oQRZoWJVgiIpKefv/7VLdAREQykBIsERFJuYR8A3/MMfHfZhj1GoiISCQx/w6WiIhIk/Tss6GHiIhIEqkHS0Qkw6TbrGK1Hc/Vt0ziW99sB8OGNbguNL84SHrLpB5T/b+U5k49WCIiIiIiInGiHiwRkQTKpG9iM6VnTETql27XA5GGUA+WiIiIiIhInKgHS0QkDan3JWTVJ1u4XbEQaZRE9kLpGiXpTAmWiIikpek/npzqJoiISAZSgiUikiKxfIOr+xfqt6nTQalugkiTF8t1KBW9ULq3S5oD3YMlIiJpqffrL9D79RdS3QwREckw6sESEZGkSPa33YPmPwXA0j5DkrpfEZFwmTSbrISoB0tERERERCRO1IMlIiIiImlP929JsqgHS0REREREJE7UgyUi0gwl8n4m/T6NiDRXibp+6T4qaQglWCIikpb+76e3pLoJIiKSgZRgiUijpNu3eel2PAJb23VIdRNEpBlR773Ei+7BEhGRtNR/8XP0X/xcqpshIiIZRgmWiIikpZMXP8fJSrBERCTJNERQRJIu3YbjaViJiIg0Rix/P+r7e5luf2ubE/VgiYiIiIiIxIl6sEQkI6iXSUREJLH0Y84h6sESERERERGJE/VgiTRzsXxb1NzGZ+ubMWmIe8ZPSXUTRCQDpGqERCz71aiOxFKCJSIiaWlnq6xUN0FERDKQEiyRJiCRPTPN7VsqfSMn8XLaS08CMP/bF6S4JSIikkl0D5aIiKSlwiXzKFwyL9XNEBGRDKMeLJFmIBU9M+oN+ppiISIikljN7b7wuiSsB8vMzjSz98xsjZlNSNR+REREREREmoqE9GCZWQvgPmAIUA4sNbPZ7r4iEfuTpiuWbyNS9U2GeitipxiKiIhITZny+SBRPVgnAWvc/QN33wn8GRieoH2JiIiIiIg0Cebu8d+o2QXAme7+o+D9D4A+7j4urMxlwGXB22OA9+LekNjkABtT3YgMo5gnn2KefIp5cineyaeYJ59innyKeXI11Xgf7u6day5M1CQXFmHZXpmcu08DpiVo/zEzsxJ3L0x1OzKJYp58innyKebJpXgnn2KefIp58inmydXc4p2oIYLlwKFh73OBjxO0LxERERERkSYhUQnWUqCbmXU1s/2BUcDsBO1LRERERESkSUjIEEF3321m44B/AC2Ah9z93UTsK4Ga7PDFNKaYJ59innyKeXIp3smnmCefYp58inlyNat4J2SSCxERERERkUyUsB8aFhERERERyTRKsEREREREROIk4xIsM8s2sxfMbHXw3LGWch3M7EkzW2VmK82sX331zexaM1tjZu+Z2dBkHVNT14CYf2hmb5tZqZmVhC3/S7CsNChTGizPM7Mvw9Y9kKRDatLiEO9JZrYuLK5nh63TOR5BHGJ+e3CtWW5mfzOzDsFyneO1iEPMdS1voGhjHpRtYWZvmtlzYct0LW+gOMRc1/MGiEO8dS1voDjEvEleyzMuwQImAC+6ezfgxeB9JPcAz7v7t4ATgJV11TezYwnNltgDOBO438xaJOwompdoYw5wmrvnh//WgbuPDJblA08BT4eVL6ta5+5jE9H4ZiimeAfuDovrHNA5Xo9YY/4CcJy79wTeB64NW6dzPLJYY65recM1JOY/5+u/m4Cu5Y0UU8wDup5HL9Z461recLHGvEleyzMxwRoOzAxezwS+U7OAmX0DOAV4EMDdd7r75nrqDwf+7O473H0tsAY4Kf7Nb5bqjXk0zMyA7wGPx6dZaSsu8a5luzrHI4sp5u5e7O67g7evEfrtQKlbrOe5ruUNF1XMzSwXOAeYXst6XcujF5eY17Jdnef7iineupY3SqzneJO8lmdignWQu1cABM8HRihzBLABeDjoipxuZm3qqX8I8FHYNsqDZRJdzAEcKDazZWZ2WYT1A4H17r46bFnX4N/oZTMbGN9mN1vxiPe4YIjDQ2Hd7TrHaxevcxzgh8DcsPc6xyOLNea6ljdctDGfAvwP8FUt63Utj148Yq7refTidY6DruXRijXmTfJanpDfwUo1M5sHfDPCquui3MR+QC/gZ+7+upndQ6jL8Ya6dhthWcbMgR+HmAP0d/ePzexA4AUzW+XuC8PWj2bvbzwrgMPcvdLMTgRmmVkPd/+iwQfQzCQ43v8H/IbQ+fsb4E5Cfyh0jif4HDez64DdwGPBoow9xyFp15V9dhthmc7zKGNuZucCn7r7MjMbVEsxXcvDJDjmup7XkIxzXNfyvSXpurJPtQjLknaOp2WC5e6Da1tnZuvNrIu7V5hZF+DTCMXKgXJ3fz14/yRfjwmtrX45cGjYNnKBj2M6kGYkDjHH3T8Onj81s78R6spdGGxjP+B84MSw8juAHcHrZWZWBhwNlJDmEhlvd18ftq0/AFU3k+ocr0WczvExwLnA6e6hHyjM5HMcEh5zXcsjiEPM+wNFFppMIQv4hpk96u4XBdvQtbyGRMZc1/N9JeEc17W8hgTHvEleyzNxiOBsYEzwegzwTM0C7v4J8JGZHRMsOh1YUU/92cAoM2tlZl2BbsCS+De/Wao35mbWxszaVb0GzgDeCSsyGFjl7uVhdTpbcMOimR1BKOYfJOQImpeY4h1coKqcx9f/DjrHaxdrzM8ErgGK3H1bWB2d47WL9bqia3nDRfP381p3z3X3PEI3mL9U9cEzoGt5w8QUc13PGyzWeOta3nCxXlea5rXc3TPqAXQiNMvI6uA5O1h+MDAnrFw+oW8WlgOzgI511Q/WXQeUAe8BZ6X6WJvKI5qYE7rv7a3g8S5wXY1tzADG1lj23aDsW8AbwLBUH2tTeMQab+AR4O3g3J8NdAlbp3M8MTFfQ2iseGnweCBYrnM8cTHXtTwBMa9RfhDwXI1lupYnMea6nic93rqWJz/mTfJabkEDREREREREJEaZOERQREREREQkIZRgiYiIiIiIxIkSLBERERERkThRgiUiIiIiIhInSrBERERERETiRAmWiIiIiIhInCjBEhERERERiRMlWCIiIiIiInGiBEtERERERCROlGCJiIiIiIjEiRIsERERERGROFGCJSIiIiIiEidKsEREJCIzm2Rmj6a6HTWZ2QIz+1Gq2xEtM+tvZqvNbKuZfcfMDjKzhWa2xczuTHX7REQkvpRgiYhkMDP7vpmVBB/+K8xsrpkNSHW7GsvMLjGzPcHxVD0GxbC9OpM5M8s1s8fMrNLM/mNmS8zs3BrFfg3c6+5t3X0WcBmwEfiGu/+/RLVNRERSQwmWiEiGMrOrgCnAb4GDgMOA+4HhKWxWPLwaJDNVjwWJ2ImZZQOLgZ1ADyAHuBv4k5ldEFb0cODdGu9XuLsnol0iIpJaSrBERDKQmbUn1LPyU3d/2t3/4+673P1Zd786rOj+ZvbHYDjbu2ZWGLaNCWZWFqxbYWbnha27xMwWm9kdZrbJzNaa2Vlh6xeY2W/M7JWgfrGZ5YSt72tm/zSzzWb2Viy9UAk0HtgKXOrun7j7l+7+OHAzcKeFlAFHAM8GvWmPA2OA/wneDzazk4JexC/MbL2Z3VW1g9riYGY3AwOBe4Pt3JvcQxcRkdoowRIRyUz9gCzgb/WUKwL+DHQAZgPhH+TLCH3Ibw9MBh41sy5h6/sA7xHq2bkNeNDMLGz994H/Bg4E9gd+CWBmhwB/B24CsoPlT5lZ5yiPrcDMNprZ+2Z2g5ntF2W9hhoCPOXuX9VY/gSh3sCj3f1I4N/AsKA3bTTwGHBb8H4ecA9wj7t/AzgyqF9nHNz9OmARMC7YzrgEHaOIiDSQEiwRkczUCdjo7rvrKbfY3ee4+x7gEeCEqhXu/ld3/9jdv3L3vwCrgZPC6v7L3f8Q1J0JdCE0FLHKw+7+vrt/SSipyA+WXwTMCfb7lbu/AJQAZ0dxXAuB4wglbd8FRgNX11mj8XKAigjLK8LWR2MXcJSZ5bj7Vnd/LVgeSxxERCRFlGCJiGSmSiAnit6dT8JebwOyquqY2cVmVhoMX9tMKLHJiVTX3bcFL9vWse2qdYcDI6q2G2x7AKEErU7u/oG7rw0SkrcJDYO8IFJZM/tV2EQYD9S37Qg21tKmLmHro3EpcDSwysyWhk2S0eg4iIhI6iRq2ISIiDRtrwLbge8ATza0spkdDvwBOJ3QpBJ7zKwUsDorRucj4BF3/3EctuXU0iZ3/y2hCT4aax7wXTObXGOY4PcIHcP7UTXQfTUw2sz+CzgfeNLMOlF/HDRJhohIE6QeLBGRDOTunwMTgfuC32Y6wMxamtlZZnZbFJtoQ+gD/gYAM/tvQj1Y8fAoMMzMhppZCzPLMrNBZpZbX8Wg/QcFr78F3AA8E6d21XQ38A1C95Z9M2jnaOA64OpoZwk0s4uC+6q+AjYHi/dQfxzWE5pAQ0REmhAlWCIiGcrd7wKuAq4nlCh9BIwDZkVRdwVwJ6GesPXA8cArcWrXR4Smiv9VWLuuJrq/WacDy83sP8Ac4Gli66Wqq52VhIbsZQErCA27vAr4QXBPWrTOBN41s62EJrwY5e7bo4jDPcAFwSyNU+NxTCIiEjvTz3CIiIiIiIjEh3qwRERERERE4kQJloiIiIiISJwowRIREREREYkTJVgiIiIiIiJx0iR+BysnJ8fz8vJS3QwREREREZGoLFu2bKO7d665vEkkWHl5eZSUlKS6GSIikk7eey/0fMwxqW2HiIikJTP7V6TlTSLBEhERibvLLw89L1iQ0maIiEhmUYIlIiIiErh0xtI61z94Se8ktUREmitNciEiIiIiIhIn6sESERGJs3TrBUm344lFXbHIpDg0Fbt27aK8vJzt27enuimSxrKyssjNzaVly5ZRlVeCJSIiIiLNUnl5Oe3atSMvLw8zS3VzJA25O5WVlZSXl9O1a9eo6ijBEhGR9HT99fUWUW+ESPO2fft2JVeSUGZGp06d2LBhQ9R1lGCJiEh6Gjw41S0QkSRQciWJ1tBzTJNciIhIeiotDT1ERESSSAmWiIikp1/8IvQQEUmgtm3b7vV+xowZjBs3LqZt5uXlsXHjxpi2EU+rVq0iPz+fgoICysrKmDp1Kt27d+fCCy9s0HY2b97M/fff36g2zJw5k27dutGtWzdmzpwZscyMGTPo3Lkz+fn55OfnM3369Hrrr127lj59+tCtWzdGjhzJzp07G9W+cEqwRERERERSZPfu3Qnfx549e2KqP2vWLIYPH86bb77JkUceyf3338+cOXN47LHHGrSdxiZYn332GZMnT+b1119nyZIlTJ48mU2bNkUsO3LkSEpLSyktLeVHP/pRvfWvueYaxo8fz+rVq+nYsSMPPvhgg9tXk+7BEhERaULqmxK9LpqYQzLeoEH7Lvve9+AnP4Ft2+Dss/ddf8klocfGjXDBBXuvW7AgpuY8++yz3HTTTezcuZNOnTrx2GOPcdBBBzFp0iQ+/vhjPvzwQ3Jycvjd737H6NGj2bBhAyeddBLuDsBtt91GVlYWV155JePHj+ett97ipZde4sUXX+Thhx/m0Ucf5YorrmDp0qV8+eWXXHDBBUyePBkI9YL98Ic/pLi4mHHjxpGdnc2NN97Ijh07OPLII3n44Yf36X0rLS1l7NixbNu2jSOPPJKHHnqIV199lSlTptCiRQsWLlzIMcccwwcffEBRURE//OEP6dWrFz//+c+B0L1KCxcupF27dtx+++088cQT7Nixg/POO4/JkyczYcIEysrKyM/PZ8iQIdx+++1RxfEf//gHQ4YMITs7G4AhQ4bw/PPPM3r06Jjqjxo1ipdeeok//elPAIwZM4ZJkyZxxRVXRLXd2qgHS0RERESkkb788svqIWn5+flMnDixet2AAQN47bXXePPNNxk1ahS33XZb9bply5bxzDPP8Kc//YnJkyczYMAA3nzzTYqKivj3v/8NwCmnnMKiRYsAKCkpYevWrezatYvFixczcOBAAG6++WZKSkpYvnw5L7/8MsuXL6/eR1ZWFosXL2bw4MHcdNNNzJs3jzfeeIPCwkLuuuuufY7l4osv5tZbb2X58uUcf/zxTJ48mbPPPpuxY8cyfvx45s+fzwMPPMDBBx/M/PnzGT9+PHfccQf33XcfpaWlLFq0iNatW1NcXMzq1atZsmQJpaWlLFu2jIULF3LLLbdw5JFHUlpaWp1c5efn1xvjdevWceihh1a/z83NZd26dRHLPvXUU/Ts2ZMLLriAjz76qM76lZWVdOjQgf3226/e7TaEerBEREQkJo3tdVOPW3zo5wbC1NXjdMABda/PyWlUj1Xr1q0pDZtQZ8aMGZSUlACh3+kaOXIkFRUV7Ny5c6/fUSoqKqJ169YALFy4kKeffhqAc845h44dOwJw4oknsmzZMrZs2UKrVq3o1asXJSUlLFq0iKlTpwLwxBNPMG3aNHbv3k1FRQUrVqygZ8+eQGi4HMBrr73GihUr6N+/PwA7d+6kX79+ex3H559/zubNmzn11FOBUG/OiBEj6j3+/v37c9VVV3HhhRdy/vnnk5ubS3FxMcXFxRQUFACwdetWVq9ezWGHHbZP/dIoJiOq6tELF2lmv2HDhjF69GhatWrFAw88wJgxY3jppZdqrR/tdhtKPVgiIpKefvvb0ENEJEV+9rOfMW7cON5++21+//vfs3379up1bdq02atspA/2LVu2JC8vj4cffpiTTz6ZgQMHMn/+fMrKyujevTtr167ljjvu4MUXX2T58uWcc845Effh7gwZMqT63qQVK1bE5V4jgAkTJjB9+nS+/PJL+vbty6pVq3B3rr322ur9rVmzhksvvTTqbb7++uvVPYKzZ88mNze3ujcKQonrwQcfvE+9Tp060apVKwB+/OMfs2zZMoBa6+fk5LB58+bq++Bq225DqQdLRETS08knp7oFzUos9341Rc2tV6e++DfFNkv9Pv/8cw455BCAWme+g9BQwMcee4zrr7+euXPn7jWBwymnnMIdd9zBQw89xPHHH89VV13FiSeeiJnxxRdf0KZNG9q3b8/69euZO3cugyLch9a3b19++tOfsmbNGo466ii2bdtGeXk5Rx99dHWZ9u3b07FjRxYtWsTAgQN55JFHqnuz6lJWVsbxxx/P8ccfz6uvvsqqVasYOnQoN9xwAxdeeCFt27Zl3bp1tGzZknbt2rFly5Z6t9mnT5+9erY+++wzfvWrX1XHpbi4mP/93//dp15FRQVdunQBYPbs2XTv3h2AoUOHRqxvZpx22mk8+eSTjBo1ipkzZzJ8+PB621efRidYZnYo8Efgm8BXwDR3v8fMsoG/AHnAh8D33D3yNB8iIiKJ8s9/hp6VaIlIikyaNIkRI0ZwyCGH0LdvX9auXRux3I033sjo0aPp1asXp5566l5D6QYOHMjNN99Mv379aNOmDVlZWdX3X51wwgkUFBTQo0cPjjjiiOohgDV17tyZGTNmMHr0aHbs2AHATTfdtFeCBaEksGqSiyOOOIKHH3643mOcMmUK8+fPp0WLFhx77LGcddZZtGrVipUrV1YPQ2zbti2PPvooRx55JP379+e4447jrLPO4vbbbyc/P7/eYYLZ2dnccMMN9O4d+qJh4sSJ1RNWTJw4kcLCQoqKipg6dSqzZ89mv/32Izs7mxkzZtRb/9Zbb2XUqFFcf/31FBQUNKinrTYWaexhVBXNugBd3P0NM2sHLAO+A1wCfObut5jZBKCju19T17YKCwu9aqyqiIhIXFR9i1vHPRWJ6uWIpTciVbMIpqIHK5G9Mo39t22q8W9sm9O952vlypXVvRQiiRTpXDOzZe5eWLNso+/BcvcKd38jeL0FWAkcAgwHqvpAZxJKukRERERERNJeXO7BMrM8oAB4HTjI3SsglISZ2YG11LkMuAyIOKOIiIhIrFZ9soXb0+zeonSi+45EJB3FPIugmbUFngJ+4e5fRFvP3ae5e6G7F3bu3DnWZoiIiIhIBmrs7S4i0WroORZTD5aZtSSUXD3m7k8Hi9ebWZeg96oL8Gks+xAREWmK95g0xVn3mmKc5GtN8ZxJlGT1TmZlZVFZWUmnTp3i8vtFIjW5O5WVlWRlZUVdJ5ZZBA14EFjp7uE/BT0bGAPcEjw/09h9iIiINNqUKfx59rupboWIJFBubi7l5eVs2LAh1U2RNJaVlUVubm7U5WPpweoP/AB428xKg2W/IpRYPWFmlwL/Bur/CWgREZF4y8/no9JdqW6FxEA9clKfli1b0rVr11Q3Q2QvjU6w3H0xUFtf7OmN3a6IiEhczJtH93ffY2WPk1LdEhERySAxT3IhIiLSJN10E+c++1CqWyEiIhkmLtO0i4iIiDQXiZpsIpMmsRCR2qkHS0REREREJE7UgyUiIkmR7F6Dqz/ZkpDtNlfpdjwiIk2VerBERERERETiRD1YIiKSlv445tpUN6FW6k2ShmqK50wq7mXT9PzSHCjBEhGRtLS+y+GpboKIiGQgJVgiIrKXxn4r3dS+WT6hdBEAb+UPTHFLRKQpqO/a1tSuYdJ8KcESEZG0dMbzjwFKsEREJLmUYImIpEgiv03VPQwikoma4r1qknk0i6CIiIiIiEicqAdLRDKCxt6LiIhIMsTUg2VmD5nZp2b2TtiybDN7wcxWB88dY2+miIiIiIhI0xdrD9YM4F7gj2HLJgAvuvstZjYheH9NjPsREck4uo8qNtN/PDnVTZAE0r020Wmqvffp9O/XVGMsqRNTD5a7LwQ+q7F4ODAzeD0T+E4s+xAREWmMTZ0OYlOng1LdDBERyTCJmOTiIHevAAieD0zAPkREROrU+/UX6P36C6luhoiIZJiUTXJhZpcBlwEcdthhqWqGiEjMNJQvpKkN+Rk0/ykAlvYZkuKWiIhIJklED9Z6M+sCEDx/GqmQu09z90J3L+zcuXMCmiEiIiIiIpJciejBmg2MAW4Jnp9JwD5ERJKmqfXMiIgkg659Io0T6zTtjwOvAseYWbmZXUoosRpiZquBIcF7ERERERGRtBdTD5a7j65l1emxbFdEJF0k6htgfbMsIhJfup9W4iVlk1yIiIgk0v/9VAMoREQk+ZRgiUiTom8QEy9Ter+2tuuQ6iaINHmZcj2IVSxx0t+1zJOIWQRFRERSrv/i5+i/+LlUN0NERDKMerBEJG3om1gJd3KQXL0y4NwUt0RE4iXTrvPq/Wqe1IMlIiIiIiISJ+rBEslgsXwTWNc3Z5o5T0REpH76u5ae1IMlIiIiIiISJ+rBEmkGNAY78fQtooiINCf1/d3S54PUUYIlIiJp6Z7xU1LdBBERyUBKsEREJC3tbJWV6iaIiEgGUoIl0sxpiIBIZKe99CQA8799QYpbIiLStCRqkqtY9ptOn1c0yYWIiKSlwiXzKFwyL9XNEBGRDKMeLJE0pynTRURERJInYT1YZnammb1nZmvMbEKi9iMiIiIiItJUJKQHy8xaAPcBQ4ByYKmZzXb3FYnYXyZJxdjVWO7xSVR7E3nfUWN7ZurbZ6aMOxYREZHUa24jWNLpc1KierBOAta4+wfuvhP4MzA8QfsSERERERFpEszd479RswuAM939R8H7HwB93H1cWJnLgMuCt8cA78W9IbHJATamuhEZRjFPPsU8+RTz5FK8k08xTz7FPPkU8+RqqvE+3N0711yYqEkuLMKyvTI5d58GTEvQ/mNmZiXuXpjqdmQSxTz5FPPkU8yTS/FOPsU8+RTz5FPMk6u5xTtRQwTLgUPD3ucCHydoXyIiIiIiIk1CohKspUA3M+tqZvsDo4DZCdqXiIiIiIhIk5CQIYLuvtvMxgH/AFoAD7n7u4nYVwI12eGLaUwxTz7FPPkU8+RSvJNPMU8+xTz5FPPkalbxTsgkFyIiIiIiIpkoYT80LCIiIiIikmmUYImIiIiIiMRJxiVYZpZtZi+Y2erguWMt5TqY2ZNmtsrMVppZv/rqm9m1ZrbGzN4zs6HJOqamrgEx/9DM3jazUjMrCVv+l2BZaVCmNFieZ2Zfhq17IEmH1KTFId6TzGxdWFzPDlunczyCOMT89uBas9zM/mZmHYLlOsdrEYeY61reQNHGPCjbwszeNLPnwpbpWt5AcYi5rucNEId461reQHGIeZO8lmdcggVMAF50927Ai8H7SO4Bnnf3bwEnACvrqm9mxxKaLbEHcCZwv5m1SNhRNC/RxhzgNHfPD/+tA3cfGSzLB54Cng4rX1a1zt3HJqLxzVBM8Q7cHRbXOaBzvB6xxvwF4Dh37wm8D1wbtk7neGSxxlzX8oZrSMx/ztd/NwFdyxspppgHdD2PXqzx1rW84WKNeZO8lmdigjUcmBm8ngl8p2YBM/sGcArwIIC773T3zfXUHw782d13uPtaYA1wUvyb3yzVG/NomJkB3wMej0+z0lZc4l3LdnWORxZTzN292N13B29fI/TbgVK3WM9zXcsbLqqYm1kucA4wvZb1upZHLy4xr2W7Os/3FVO8dS1vlFjP8SZ5Lc/EBOsgd68ACJ4PjFDmCGAD8HDQFTndzNrUU/8Q4KOwbZQHyyS6mAM4UGxmy8zssgjrBwLr3X112LKuwb/Ry2Y2ML7NbrbiEe9xwRCHh8K623WO1y5e5zjAD4G5Ye91jkcWa8x1LW+4aGM+Bfgf4Kta1utaHr14xFzX8+jF6xwHXcujFWvMm+S1PCG/g5VqZjYP+GaEVddFuYn9gF7Az9z9dTO7h1CX4w117TbCsoyZAz8OMQfo7+4fm9mBwAtmtsrdF4atH83e33hWAIe5e6WZnQjMMrMe7v5Fgw+gmUlwvP8P+A2h8/c3wJ2E/lDoHE/wOW5m1wG7gceCRRl7jkPSriv77DbCMp3nUcbczM4FPnX3ZWY2qJZiupaHSXDMdT2vIRnnuK7le0vSdWWfahGWJe0cT8sEy90H17bOzNabWRd3rzCzLsCnEYqVA+Xu/nrw/km+HhNaW/1y4NCwbeQCH8d0IM1IHGKOu38cPH9qZn8j1JW7MNjGfsD5wIlh5XcAO4LXy8ysDDgaKCHNJTLe7r4+bFt/AKpuJtU5Xos4neNjgHOB091DP1CYyec4JDzmupZHEIeY9weKLDSZQhbwDTN71N0vCraha3kNiYy5ruf7SsI5rmt5DQmOeZO8lmfiEMHZwJjg9RjgmZoF3P0T4CMzOyZYdDqwop76s4FRZtbKzLoC3YAl8W9+s1RvzM2sjZm1q3oNnAG8E1ZkMLDK3cvD6nS24IZFMzuCUMw/SMgRNC8xxTu4QFU5j6//HXSO1y7WmJ8JXAMUufu2sDo6x2sX63VF1/KGi+bv57XunuvueYRuMH+p6oNnQNfyhokp5rqeN1is8da1vOFiva40zWu5u2fUA+hEaJaR1cFzdrD8YGBOWLl8Qt8sLAdmAR3rqh+suw4oA94Dzkr1sTaVRzQxJ3Tf21vB413guhrbmAGMrbHsu0HZt4A3gGGpPtam8Ig13sAjwNvBuT8b6BK2Tud4YmK+htBY8dLg8UCwXOd44mKua3kCYl6j/CDguRrLdC1PYsx1PU96vHUtT37Mm+S13IIGiIiIiIiISIwycYigiIiIiIhIQijBEhERERERiRMlWCIiIiIiInGiBEtERERERCROlGCJiIiIiIjEiRIsERERERGROFGCJSIiIiIiEidKsEREREREROJECZaIiIiIiEicKMESERERERGJEyVYIiIiIiIicaIES0REREREJE6UYImISDUzm2Rmj6a6HTWZ2QIz+1Gq2xErMzvPzD4ys61mVmBmx5jZm2a2xcyuTHX7REQkdkqwREQyjJl938xKgg/5FWY218wGpLpdjWVml5jZnuB4qh6DYtzmuWa2xMz+Y2aVZvaomR1ST51jzWy2mX0eJEzzzezkGsXuAMa5e1t3fxP4H2CBu7dz96kxtPdDMxvc2PoiIhI/SrBERDKImV0FTAF+CxwEHAbcDwxPYbPi4dUgaal6LGjshszsAuBPwD1ADtAD2AksMrMOtdQ5EngFeBvoChwM/A0oNrN+YUUPB96t472IiDRzSrBERDKEmbUHfg381N2fdvf/uPsud3/W3a8OK7q/mf0x6IV518wKw7YxwczKgnUrzOy8sHWXmNliM7vDzDaZ2VozOyts/QIz+42ZvRLULzaznLD1fc3sn2a22czeirUXqjHMzIA7gZvc/TF3/9LdPwF+BGwDfl5L1UmEkrzr3P0zd98S9Eg9AtxqZq3MbCvQAngriOFLwGnAvUGv29FmdnYQ1y1mts7MfhnWtnPNrDSIzz/NrGew/BFCifKzwXb+JzHRERGRaCjBEhHJHP2ALEI9K3UpAv4MdABmA/eGrSsDBgLtgcnAo2bWJWx9H+A9Qj0/twEPBklLle8D/w0cCOwP/BIgGH73d+AmIDtY/pSZdY7y2ArMbKOZvW9mN5jZflHWq+kYQsnKX8MXuvtXwFPAGbXUG1KzTuAJoD/Qwt3bBstOcPcj3f3bwCK+HjL4PvAgcLm7twOOA14CMLNewEPA5UAn4PfAbDNr5e4/AP4NDAu2c1sjj11EROJACZaISOboBGx09931lFvs7nPcfQ+hHpgTqla4+1/d/WN3/8rd/wKsBk4Kq/svd/9DUHcm0IXQUMQqD7v7++7+JaHkIz9YfhEwJ9jvV+7+AlACnB3FcS0klIwcCHwXGA1cXWeN2lX1qFVEWFcB1Jbw5dRR57+AjlHufxdwrJl9w903ufsbwfIfA79399fdfY+7zwR2AH2j3K6IiCSJEiwRkcxRCeRE0bvzSdjrbUBWVR0zuzhsmNpmQolNTqS67r4teNk20vpg21XrDgdGVG032PYAQglandz9A3dfGyRmbxMaBnlBpLJm9quwiTAeiFBkY/Acab9dgA21NGNjHXW+AjbVeRBf+y6hpPJfZvZy2P1bhwP/r0Z8DiV0r5eIiDQhSrBERDLHq8B24DuNqWxmhwN/AMYBndy9A/AOYHXVi9JHwCPu3iHs0cbdb2nEtry2Nrn7b8Mmwhgboch7QDkwInyhmf0XoeTn5Vr2Oa9mncD3CN2btS3CukjtW+ruwwn1xs0i1MsHofjcXCM+B7j741VVo9m+iIgknhIsEZEM4e6fAxOB+8zsO2Z2gJm1NLOzzCya+3baEPogvwHAzP6bUA9WPDwKDDOzoWbWwsyyzGyQmeXWVzFo/0HB628BNwDPNKYR7u6E7v+63kLT2bc2s28C0wn11P2ulqqTgZPN7GYzyzazdmb2M+Bi4Jpo9m1m+5vZhWbW3t13AV8Ae4LVfwDGmlkfC2ljZueYWbtg/XrgiMYcs4iIxJcSLBGRDOLudwFXAdcTSpQ+ItQjNSuKuisIzbD3KqEP9McTmpo8Hu36iNBU8b8Ka9fVRPd36nRguZn9B5gDPE1oGvrGtuUvwA+A8YSGVVYAvYFT3T3SfVa4+2pCQxpPAD4M6nwXGOruDYnRD4APzewLYCyhe9Nw9xJC92HdS2i44RrgkrB6/0soKdwcPvOgiIgkn4W+rBMREZFIzOwM4HHgdHcvTXFzRESkiVOCJSIiUg8zGwYc4u6RJsYQERGppgRLREREREQkTnQPloiIiIiISJwowRIREREREYmT+n5sMilycnI8Ly8v1c0QERERERGJyrJlyza6e+eay5tEgpWXl0dJSUmqmyEiIunkvfdCz8cck9p2iIhIWjKzf0Va3iQSLBERkbi7/PLQ84IFKW2GiIhkFiVYIiIi0uxcOmNpnesfvKR3kloiIrI3TXIhIiIiIiISJ+rBEhERkbRTVw+XerfSx65duygvL2f79u2pboqksaysLHJzc2nZsmVU5ZVgiYiIiEizVF5eTrt27cjLy8PMUt0cSUPuTmVlJeXl5XTt2jWqOkqwREQkPV1/fapbICIJtn37diVXklBmRqdOndiwYUPUdZRgiYhIeho8ONUtEJEkUHIlidbQc0yTXIiISHoqLQ09REREkkgJloiIpKdf/CL0EBFJoLZt2+71fsaMGYwbNy6mbebl5bFx48aYthFPq1atIj8/n4KCAsrKypg6dSrdu3fnwgsvbNB2Nm/ezP3339+oNsycOZNu3brRrVs3Zs6cGbHMjBkz6Ny5M/n5+eTn5zN9+vR6669du5Y+ffrQrVs3Ro4cyc6dOxvVvnAaIigiIiKadU8kRXbv3s1++yX2I/mePXto0aJFo+vPmjWL4cOHM3nyZADuv/9+5s6dG/WkD1WqEqyf/OQnDar32WefMXnyZEpKSjAzTjzxRIqKiujYseM+ZUeOHMm9994bdf1rrrmG8ePHM2rUKMaOHcuDDz7IFVdc0aD21aQeLBERERFJD4MG7fuo6jHZti3y+hkzQus3btx3XYyeffZZ+vTpQ0FBAYMHD2b9+vUATJo0icsuu4wzzjiDiy++mMrKSs444wwKCgq4/PLLcXcAbrvtNqZOnQrA+PHj+fa3vw3Aiy++yEUXXQTAFVdcQWFhIT169ODGG2+s3ndeXh6//vWvGTBgAH/9618pLi6mX79+9OrVixEjRrB169Z92ltaWkrfvn3p2bMn5513Hps2bWLOnDlMmTKF6dOnc9pppzF27Fg++OADioqKuPvuu3n55Zere4wKCgrYsmULALfffju9e/emZ8+e1e2aMGECZWVl5Ofnc/XVV0cdx3/84x8MGTKE7OxsOnbsyJAhQ3j++edjru/uvPTSS1xwwQUAjBkzhlmzZkW93dqoB0tERKSZqKuXCdTTJJIKX375Jfn5+dXvP/vsM4qKigAYMGAAr732GmbG9OnTue2227jzzjsBWLZsGYsXL6Z169ZceeWVDBgwgIkTJ/L3v/+dadOmAXDKKadw5513cuWVV1JSUsKOHTvYtWsXixcvZuDAgQDcfPPNZGdns2fPHk4//XSWL19Oz549gdDvNy1evJiNGzdy/vnnM2/ePNq0acOtt97KXXfdxcSJE/c6losvvpjf/e53nHrqqUycOJHJkyczZcoUxo4dS9u2bfnlL38JwPPPP8/8+fPJyclh2LBh3HffffTv35+tW7eSlZVFcXExq1evZsmSJbg7RUVFLFy4kFtuuYV33nmH0rD7Y/Pz8/d6H8m6des49NBDq9/n5uaybt26iGWfeuopFi5cyNFHH83dd9/NoYceWmv9yspKOnToUN2DWNd2G0IJloiIiCSMhh5+TbFIggULal93wAF1r8/JqXt9LVq3br1XgjBjxgxKSkqA0O90jRw5koqKCnbu3LnXkLqioiJat24NwMKFC3n66acBOOecc6qHvp144oksW7aMLVu20KpVK3r16kVJSQmLFi2q7tl64oknmDZtGrt376aiooIVK1ZUJ1gjR44E4LXXXmPFihX0798fgJ07d9KvX7+9juPzzz9n8+bNnHrqqUCoN2fEiBH1Hn///v256qqruPDCCzn//PPJzc2luLiY4uJiCgoKANi6dSurV6/msMMO26d+fckVUN2jFy7SzH7Dhg1j9OjRtGrVigceeIAxY8bw0ksv1Vo/2u02lIYIiohIevrtb0MPEZEU+dnPfsa4ceN4++23+f3vf8/27dur17Vp02avspE+2Lds2ZK8vDwefvhhTj75ZAYOHMj8+fMpKyuje/furF27ljvuuIMXX3yR5cuXc84550Tch7szZMgQSktLKS0tZcWKFTz44INxOcYJEyYwffp0vvzyS/r27cuqVatwd6699trq/a1Zs4ZLL7006m2+/vrr1cMOZ8+eTW5uLh999FH1+vLycg4++OB96nXq1IlWrVoB8OMf/5hly5YB1Fo/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dNjR9RcSkbTSsmVLunbtmupmSIbTEEEREUlL3d9dQvd3l6S6GSIikmHUgyUiImnp3GcfAmBlj5NS3BIREckk6sESERERERGJEyVYIiIiIiIicaIES0REREREJE50D5aIiCTFpTOWproJSVPfsT54Se8ktURERJJNCZaIiKSlP465NtVNEBGRDKQES0RE0tL6LoenugkiIpKBdA+WiIikpRNKF3FC6aJUN0NERDJMTD1YZvYhsAXYA+x290Izywb+AuQBHwLfc/dNsTVTRESkYc54/jEA3sofmOKWiIhIJonHEMHT3H1j2PsJwIvufouZTQjeXxOH/YiISBOWSZNYQOYdr4iIRCcRQwSHAzOD1zOB7yRgHyIiIiIiIk1OrAmWA8VmtszMLguWHeTuFQDB84GRKprZZWZWYmYlGzZsiLEZIiIiIiIiqRfrEMH+7v6xmR0IvGBmq6Kt6O7TgGkAhYWFHmM7REREREREUi6mBMvdPw6ePzWzvwEnAevNrIu7V5hZF+DTOLRTRESkQab/eHKqmyAiIhmo0UMEzayNmbWreg2cAbwDzAbGBMXGAM/E2kgREZGG2tTpIDZ1OijVzRARkQwTSw/WQcDfzKxqO39y9+fNbCnwhJldCvwbGBF7M0VERBqm9+svALC0z5AUt0RERDJJoxMsd/8AOCHC8krg9FgaJSIiEqtB858ClGCJiEhyJWKadhERERERkYwUjx8aFhGRDKEf1xUREamberBERERERETiRAmWiIiIiIhInGiIoIiIpKX/++ktqW5CrRo71PLBS3rHuSUiIhJvSrBERGQv6XKf1dZ2HVLdBBERyUAaIigiImmp/+Ln6L/4uVQ3Q0REMox6sEREJC2dHCRXrww4N8UtiZ/6ehc1hFBEJPXUgyUiIiIiIhInSrBERERERETiRAmWiIiIiIhInCjBEhERERERiRNNciEikmHSZRr2+twzfkqqmyAiIhlICZaIiKSlna2yUt0EERHJQEqwREQkLZ320pMAzP/2BSluSfLU1TupKdxFRJJDCZaISBrKlGGAdSlcMg/IrARLRERST5NciIiIiIiIxEnCerDM7EzgHqAFMN3db0nUvkRERKRusfRqanihiEj0EpJgmVkL4D5gCFAOLDWz2e6+IhH7ExFpjjSMTzKB7gsTkUyTqB6sk4A17v4BgJn9GRgOKMESkbSj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zwcGyw8BPgorVx4sExERSapD//0+lJamuhkikmHatm271/sZM2Ywbty4mLaZl5fHxo0bY9pGPK1atYr8/HwKCgooKytj6tSpdO/enQsvvLBB29m8eTP3339/VGXXrl1Lnz596NatGyNHjmTnzp0Ry82cOZNu3brRrVs3Zs6cWW99d+fKK6/kqKOOomfPnrzxxhsNOoZI4j2LYKT0ziMWNLvMzErMrGTDhg1xboaIiGS6UX+6C37xi1Q3Q0SkQXbv3p3wfezZsyem+rNmzWL48OG8+eabHHnkkdx///3MmTOHxx57rEHbaUiCdc011zB+/HhWr15Nx44defDBB/cp89lnnzF58mRef/11lixZwuTJk9m0aVOd9efOncvq1atZvXo106ZN44orrmjQMUTS2ARrvZl1AQiePw2WlwOHhpXLBT6OtAF3n+buhe5e2Llz50Y2Q0RERESkeXj22Wfp06cPBQUFDB48mPXr1wMwadIkLrvsMs444wwuvvhiKisrOeOMMygoKODyyy/HPdRfcdtttzF16lQAxo8fz7e//W0AXnzxRS666CIArrjiCgoLC+nRowc33nhj9b7z8vL49a9/zYABA/jrX/9KcXEx/fr1o1evXowYMYKtW7fu097S0lL69u1Lz549Oe+889i0aRNz5sxhypQpTJ8+ndNOO42xY8fywQcfUFRUxN13383LL79Mfn5+dQ/Xli1bALj99tvp3bs3PXv2rG7XhAkTKCsrIz8/n6uvvrrWuLk7L730EhdccAEAY8aMYdasWfuU+8c//sGQIUPIzs6mY8eODBkyhOeff77O+s888wwXX3wxZkbfvn3ZvHkzFRUV0f2D1qKx07TPBsYAtwTPz4Qt/5OZ3QUcDHQDlsTUQhERkUZa9ckWbq/lh4gBHrykdxJbIyJJN2jQvsu+9z34yU9g2zY4++x9119ySeixcSMEH8irLVhQ7y6//PJL8vPzq99/9tlnFBUVATBgwABee+01zIzp06dz2223ceeddwKwbNkyFi9eTOvWrbnyyisZMGAAEydO5O9//zvTpk0D4JRTTuHOO+/kyiuvpKSkhB07drBr1y4WL17MwIEDAbj55pvJzs5mz549nH766SxfvpyePXsCoenGFy9ezMaNGzn//POZN28ebdq04dZbb+Wuu+5i4sSJex3LxRdfzO9+9ztOPfVUJk6c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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "class TestProgram(AveragerProgramV2):\n", + " \"\"\"Simple program to measure IQ offsets across multiple readout channels.\"\"\"\n", + " \n", + " def _initialize(self, cfg):\n", + " for ch in cfg['ro_chs']:\n", + " self.declare_readout(ch=ch, length=cfg['ro_len'], freq=100)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=cfg['ro_chs'], t=0.0)\n", + "\n", + "config_offsets = {\n", + " 'ro_chs': MUXRO_CH + [STDRO_CH],\n", + " 'ro_len': 100.0, # µs, long readout to average noise\n", + "}\n", + "\n", + "print(\"Measuring IQ offsets...\")\n", + "prog = TestProgram(soccfg, reps=1000, final_delay=0.5, cfg=config_offsets)\n", + "\n", + "# Acquire data (offsets are automatically subtracted)\n", + "iq_list = prog.acquire(soc, rounds=1)\n", + "\n", + "# Get raw data (to see the offsets)\n", + "iq_raw = prog.get_raw()\n", + "\n", + "nch = len(prog.ro_chs)\n", + "fig, axes = plt.subplots(2 * nch, 1, figsize=(12, 3 * nch))\n", + "for i, (ch, rocfg) in enumerate(prog.ro_chs.items()):\n", + " offset = soccfg['readouts'][ch]['iq_offset']\n", + " \n", + " # Check what iq_offset actually looks like\n", + " print(f\"ch {ch} iq_offset: {offset}, type: {type(offset)}\")\n", + " \n", + " # Handle both float and list formats\n", + " if isinstance(offset, (list, tuple, np.ndarray)):\n", + " offset_i, offset_q = offset[0], offset[1]\n", + " else:\n", + " offset_i = offset_q = offset # single value applies to both\n", + " \n", + " bins_i = np.linspace(offset_i - 0.1, offset_i + 0.1, 101)\n", + " bins_q = np.linspace(offset_q - 0.1, offset_q + 0.1, 101)\n", + " nsamp = rocfg['length']\n", + " \n", + " axes[2 * i].hist(iq_raw[i][:, 0, 0] / nsamp, bins=bins_i, alpha=0.7)\n", + " axes[2 * i].axvline(offset_i, color='red', linestyle='--',\n", + " label=f'Hardware offset: {offset_i:.3f}')\n", + " axes[2 * i].set_title(f\"Channel {ch} - I Offset\")\n", + " axes[2 * i].legend()\n", + " \n", + " axes[2 * i + 1].hist(iq_raw[i][:, 0, 1] / nsamp, bins=bins_q, alpha=0.7)\n", + " axes[2 * i + 1].axvline(offset_q, color='red', linestyle='--',\n", + " label=f'Hardware offset: {offset_q:.3f}')\n", + " axes[2 * i + 1].set_title(f\"Channel {ch} - Q Offset\")\n", + " axes[2 * i + 1].legend()\n", + "\n", + "plt.tight_layout()\n", + "print(\"\\nNote: The software automatically subtracts these hardware offsets from acquired data.\")\n", + "print(\"You can manually access raw data via prog.get_raw() if needed.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Performance Benchmarks\n", + "\n", + "Understanding the performance characteristics of different configurations helps optimize your experiments." + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Benchmarking different readout types...\n", + "------------------------------------------------------------\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "07b8ac09ceea4c34a977930d0e331d05", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1000 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "── Signal diagnosis ──────────────────────────────────────────────────\n", + "⚠️ Magnitude highly variable — trigger may be firing before pulse arrives.\n", + " → Swap pulse/trigger order in _body, or increase trig_time.\n" + ] + } + ], + "source": [ + "class PhaseTrackingProgram(AveragerProgramV2):\n", + " \"\"\"\n", + " Program for tracking shot-to-shot phase stability.\n", + " Useful for qubit stability measurements and noise characterization.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'], freq=cfg['freq'], gen_ch=gen_ch)\n", + " \n", + " self.add_pulse(ch=gen_ch, name=\"probe\", style=\"const\", \n", + " length=cfg['pulse_len'], freq=cfg['freq'], \n", + " phase=cfg['phase'], gain=cfg['gain'])\n", + " \n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], t=cfg['trig_time'])\n", + " self.pulse(ch=cfg['gen_ch'], name=\"probe\", t=0)\n", + "\n", + "# Track phase over many shots\n", + "# Phase tracking using get_raw() to recover per-rep IQ data\n", + "n_shots = 500\n", + "config_tracking = {\n", + " 'gen_ch': FSGEN_CH,\n", + " 'ro_ch': STDRO_CH,\n", + " 'freq': 500,\n", + " 'gain': 0.3,\n", + " 'phase': 0,\n", + " 'trig_time': 0.4,\n", + " 'ro_len': 0.5,\n", + " 'pulse_len': 0.3,\n", + "}\n", + "\n", + "print(f\"Tracking phase over {n_shots} shots...\")\n", + "prog_track = PhaseTrackingProgram(soccfg, reps=n_shots, final_delay=0.1, cfg=config_tracking)\n", + "iq_list = prog_track.acquire(soc, rounds=1)\n", + "iq_raw = prog_track.get_raw()\n", + "\n", + "# Extract per-rep IQ points\n", + "# iq_raw shape: (n_ro_channels, n_reps, n_trigs, 2)\n", + "nsamp = prog_track.ro_chs[STDRO_CH]['length']\n", + "iq_shots = iq_raw[0][:, 0, :] # (n_reps, 2)\n", + "i_vals = iq_shots[:, 0] / nsamp\n", + "q_vals = iq_shots[:, 1] / nsamp\n", + "phases_shots = np.angle(i_vals + 1j * q_vals, deg=True)\n", + "\n", + "print(f\"n_shots recovered: {len(phases_shots)}\")\n", + "print(f\"Mean I: {np.mean(i_vals):.4f}\")\n", + "print(f\"Mean Q: {np.mean(q_vals):.4f}\")\n", + "print(f\"Mean magnitude: {np.mean(np.abs(i_vals + 1j*q_vals)):.4f}\")\n", + "print(f\"Std magnitude: {np.std( np.abs(i_vals + 1j*q_vals)):.4f}\")\n", + "print(f\"Phase stability: {np.std(phases_shots):.3f} degrees RMS\")\n", + "\n", + "# ── Diagnostics ────────────────────────────────────────────────────────────────\n", + "fig, axes = plt.subplots(2, 2, figsize=(14, 10))\n", + "\n", + "# 1. IQ cloud\n", + "axes[0, 0].scatter(i_vals, q_vals, alpha=0.3, s=10)\n", + "axes[0, 0].axhline(0, color='k', linewidth=0.5)\n", + "axes[0, 0].axvline(0, color='k', linewidth=0.5)\n", + "axes[0, 0].set_xlabel(\"I\")\n", + "axes[0, 0].set_ylabel(\"Q\")\n", + "axes[0, 0].set_title(\"IQ Cloud (tight cluster = good signal)\")\n", + "axes[0, 0].axis('equal')\n", + "axes[0, 0].grid(True)\n", + "\n", + "# 2. Per-shot magnitude\n", + "magnitudes = np.abs(i_vals + 1j * q_vals)\n", + "axes[0, 1].plot(magnitudes, '.-', alpha=0.5, markersize=3)\n", + "axes[0, 1].set_xlabel(\"Shot\")\n", + "axes[0, 1].set_ylabel(\"Magnitude\")\n", + "axes[0, 1].set_title(\"Per-shot Magnitude (stable & non-zero = signal present)\")\n", + "axes[0, 1].axhline(np.mean(magnitudes), color='red', label=f\"Mean: {np.mean(magnitudes):.4f}\")\n", + "axes[0, 1].legend()\n", + "axes[0, 1].grid(True)\n", + "\n", + "# 3. Phase vs shot\n", + "axes[1, 0].plot(phases_shots, '.-', alpha=0.5, markersize=3)\n", + "axes[1, 0].set_ylabel(\"Phase (degrees)\")\n", + "axes[1, 0].set_xlabel(\"Shot Number\")\n", + "axes[1, 0].set_title(\"Shot-to-Shot Phase Stability\")\n", + "axes[1, 0].axhline(np.mean(phases_shots), color='red',\n", + " label=f\"Mean: {np.mean(phases_shots):.1f}°\")\n", + "axes[1, 0].axhline(np.mean(phases_shots) + np.std(phases_shots), color='orange',\n", + " linestyle='--', label=f\"±1σ: {np.std(phases_shots):.2f}°\")\n", + "axes[1, 0].axhline(np.mean(phases_shots) - np.std(phases_shots), color='orange',\n", + " linestyle='--')\n", + "axes[1, 0].legend()\n", + "axes[1, 0].grid(True)\n", + "\n", + "# 4. Phase histogram\n", + "axes[1, 1].hist(phases_shots, bins=50, alpha=0.7)\n", + "axes[1, 1].set_xlabel(\"Phase (degrees)\")\n", + "axes[1, 1].set_ylabel(\"Counts\")\n", + "axes[1, 1].set_title(f\"Phase Distribution (σ = {np.std(phases_shots):.3f}°)\")\n", + "axes[1, 1].grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# ── Interpretation ─────────────────────────────────────────────────────────────\n", + "mag_mean = np.mean(magnitudes)\n", + "mag_std = np.std(magnitudes)\n", + "phase_std = np.std(phases_shots)\n", + "\n", + "print(\"\\n── Signal diagnosis ──────────────────────────────────────────────────\")\n", + "if mag_mean < 0.01:\n", + " print(\"❌ Magnitude near zero — no signal reaching ADC.\")\n", + " print(\" → Check wiring, increase gain, verify frequency is on resonance.\")\n", + "elif mag_std / mag_mean > 0.5:\n", + " print(\"⚠️ Magnitude highly variable — trigger may be firing before pulse arrives.\")\n", + " print(\" → Swap pulse/trigger order in _body, or increase trig_time.\")\n", + "elif phase_std > 10:\n", + " print(\"⚠️ Large phase scatter despite signal present.\")\n", + " print(\" → Frequency may be off resonance, or DDS phase not reset between reps.\")\n", + " print(\" → Try adding phrst=1 to the pulse, or scan freq to find resonance.\")\n", + "else:\n", + " print(f\"✅ Good signal. Phase stability: {phase_std:.3f}° RMS\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 10. Common Pitfalls and Troubleshooting\n", + "\n", + "This section covers frequently encountered issues and their solutions." + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ COMMON PITFALLS AND SOLUTIONS ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ Issue │ Likely Cause │ Solution ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ No signal in readout │ Wrong physical connection │ Check DAC→ADC loopback cables ║\n", + "║ │ Generator not enabled │ Verify gen_ch is correct ║\n", + "║ │ Wrong nqz setting │ nqz=1 for Nyquist zone 1 ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Phase rotates with frequency │ Electrical delay not calibrated │ Run calibration sweep (see Section 5) ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Muxed readout wrong phases │ No frequency matching │ Set gen_ch in declare_readout() ║\n", + "║ │ Tone order mismatch │ Ensure frequencies match order ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Interpolated gen freq off │ Forgot mixer_freq in tProc v2 │ Use absolute frequencies, not IFs ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ PFB channels swapped │ Tone order mismatch │ Verify pulse_freqs order matches ro_chs ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Acquisition too slow │ Too many reps without averaging │ Use software averaging or reduce reps ║\n", + "║ │ Long final_delay │ Minimize final_delay when possible ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Distorted pulse shape │ Pulse too short for ramp │ Ensure flat_len >= ramp_len/2 ║\n", + "║ │ DAC saturation │ Reduce gain ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Memory errors │ Too many pulses/readouts │ Use loops and dynamic configs ║\n", + "║ │ Pulse/readout length too long │ Limit to < 2^15 samples ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Fast dynamic readout issues │ Wrong channel type │ Use FASTDYNRO_CH, not DYNRO_CH ║\n", + "║ │ Missing send_readoutconfig │ Send config before each trigger ║\n", + "║ ║\n", + "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n", + "\n" + ] + } + ], + "source": [ + "print(\"\"\"\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ COMMON PITFALLS AND SOLUTIONS ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ Issue │ Likely Cause │ Solution ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ No signal in readout │ Wrong physical connection │ Check DAC→ADC loopback cables ║\n", + "║ │ Generator not enabled │ Verify gen_ch is correct ║\n", + "║ │ Wrong nqz setting │ nqz=1 for Nyquist zone 1 ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Phase rotates with frequency │ Electrical delay not calibrated │ Run calibration sweep (see Section 5) ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Muxed readout wrong phases │ No frequency matching │ Set gen_ch in declare_readout() ║\n", + "║ │ Tone order mismatch │ Ensure frequencies match order ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Interpolated gen freq off │ Forgot mixer_freq in tProc v2 │ Use absolute frequencies, not IFs ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ PFB channels swapped │ Tone order mismatch │ Verify pulse_freqs order matches ro_chs ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Acquisition too slow │ Too many reps without averaging │ Use software averaging or reduce reps ║\n", + "║ │ Long final_delay │ Minimize final_delay when possible ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Distorted pulse shape │ Pulse too short for ramp │ Ensure flat_len >= ramp_len/2 ║\n", + "║ │ DAC saturation │ Reduce gain ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Memory errors │ Too many pulses/readouts │ Use loops and dynamic configs ║\n", + "║ │ Pulse/readout length too long │ Limit to < 2^15 samples ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────────║\n", + "║ Fast dynamic readout issues │ Wrong channel type │ Use FASTDYNRO_CH, not DYNRO_CH ║\n", + "║ │ Missing send_readoutconfig │ Send config before each trigger ║\n", + "║ ║\n", + "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 11. Summary and Best Practices" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ BEST PRACTICES SUMMARY ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ ✓ Always calibrate electrical delay before precision phase measurements ║\n", + "║ ✓ Use muxed generators + PFB readout for multi-tone experiments ║\n", + "║ ✓ In tProc v2, specify ABSOLUTE frequencies (not IFs) ║\n", + "║ ✓ Link generators to readouts with gen_ch parameter for automatic frequency matching ║\n", + "║ ✓ Use dynamic readouts for frequency sweeps to save configuration time ║\n", + "║ ✓ Keep pulse lengths within BRAM constraints (< 2^15 samples @ 1ns sample rate = 32.768 µs) ║\n", + "║ ✓ Use software averaging (reps parameter) for noise reduction ║\n", + "║ ✓ Monitor IQ offsets and apply corrections when necessary ║\n", + "║ ✓ Test new configurations with low power and short pulses first ║\n", + "║ ✓ Use progress=True for long acquisitions to monitor status ║\n", + "║ ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ WHEN TO USE EACH TOOL ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ • Full-Speed Generator: Single tone, maximum bandwidth, simple pulses ║\n", + "║ • Interpolated Generator: Fine frequency resolution, shaped pulses ║\n", + "║ • Muxed Generators: Multiple phase-coherent tones ║\n", + "║ • Standard Readout: Fixed-frequency averaging ║\n", + "║ • Dynamic Readout: Frequency sweeps, calibration ║\n", + "║ • Fast Dynamic Readout: Phase-reset applications, rapid sequencing ║\n", + "║ • PFB Readout: Multi-tone demodulation with 64 channels ║\n", + "║ ║\n", + "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n", + "\n" + ] + } + ], + "source": [ + "print(\"\"\"\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ BEST PRACTICES SUMMARY ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ ✓ Always calibrate electrical delay before precision phase measurements ║\n", + "║ ✓ Use muxed generators + PFB readout for multi-tone experiments ║\n", + "║ ✓ In tProc v2, specify ABSOLUTE frequencies (not IFs) ║\n", + "║ ✓ Link generators to readouts with gen_ch parameter for automatic frequency matching ║\n", + "║ ✓ Use dynamic readouts for frequency sweeps to save configuration time ║\n", + "║ ✓ Keep pulse lengths within BRAM constraints (< 2^15 samples @ 1ns sample rate = 32.768 µs) ║\n", + "║ ✓ Use software averaging (reps parameter) for noise reduction ║\n", + "║ ✓ Monitor IQ offsets and apply corrections when necessary ║\n", + "║ ✓ Test new configurations with low power and short pulses first ║\n", + "║ ✓ Use progress=True for long acquisitions to monitor status ║\n", + "║ ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ WHEN TO USE EACH TOOL ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ • Full-Speed Generator: Single tone, maximum bandwidth, simple pulses ║\n", + "║ • Interpolated Generator: Fine frequency resolution, shaped pulses ║\n", + "║ • Muxed Generators: Multiple phase-coherent tones ║\n", + "║ • Standard Readout: Fixed-frequency averaging ║\n", + "║ • Dynamic Readout: Frequency sweeps, calibration ║\n", + "║ • Fast Dynamic Readout: Phase-reset applications, rapid sequencing ║\n", + "║ • PFB Readout: Multi-tone demodulation with 64 channels ║\n", + "║ ║\n", + "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 12. Next Steps\n", + "\n", + "You have now explored the key advanced generator and readout blocks in tProc v2.\n", + "\n", + "Proceed to [`08_Hardware_Buffers.ipynb`](./08_Hardware_Buffers.ipynb) to learn about the DDR4 and MR buffers for high-speed data capture.\n", + "\n", + "**Key Takeaways:**\n", + "\n", + "1. **tProc v2 uses absolute frequencies** - no more manual IF calculations\n", + "2. **Link generators and readouts** with `gen_ch` for automatic frequency matching\n", + "3. **Calibrate electrical delay** for accurate phase measurements\n", + "4. **Choose the right tool** based on your application requirements\n", + "5. **Monitor hardware offsets** and apply corrections as needed\n", + "\n", + "**Additional Resources:**\n", + "- To learn about basic sequencing, proceed to [`01_Basic_Sequencing.ipynb`](./01_Basic_Sequencing.ipynb)\n", + "- For parameter sweeps, see [`02_Parameter_Sweeps.ipynb`](./02_Parameter_Sweeps.ipynb)\n", + "- For real-time feedback, see [`04_Real_Time_Feedback.ipynb`](./04_Real_Time_Feedback.ipynb)\n", + "- For multi-core synchronization, see [`10_Multi_Core_Synchronization.ipynb`](./10_Multi_Core_Synchronization.ipynb)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/08_Hardware_Buffers.ipynb b/docs/source/tutorials/08_Hardware_Buffers.ipynb new file mode 100644 index 000000000..06e84c582 --- /dev/null +++ b/docs/source/tutorials/08_Hardware_Buffers.ipynb @@ -0,0 +1,661 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 08 - Hardware Buffers: DDR4 and MR\n", + "\n", + "**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." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Understanding Hardware Buffers\n", + "\n", + "QICK provides two types of hardware buffers for capturing readout data:\n", + "\n", + "| Buffer | Size | Access | Best for |\n", + "| :--- | :--- | :--- | :--- |\n", + "| **DDR4 Buffer** | Very large (up to ~1 second of data) | Read from Python after experiment | Long acquisitions, many repetitions |\n", + "| **MR Buffer** | Small (typically 8192 samples) | Read by tProc during experiment | Fast feedback, real-time decision making |\n", + "\n", + "**Key concepts:**\n", + "- Both buffers capture **decimated** IQ data (after downconversion and filtering)\n", + "- You can enable one or both in the same experiment\n", + "- Data is written continuously; you must manage buffer overflow" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Capturing Data to DDR4 Buffer\n", + "\n", + "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." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6fff0ae3d25a4a719b077421df619a43", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/60 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Captured 31599 IQ samples from DDR4 buffer\n", + "Total duration: 102.86 µs\n" + ] + } + ], + "source": [ + "class DDR4Program(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define loops (outer = gain sweep, inner = phase sweep)\n", + " self.add_loop(\"gain_loop\", self.cfg[\"steps_gain\"])\n", + " self.add_loop(\"phase_loop\", self.cfg[\"steps_phase\"])\n", + "\n", + " # Define a Gaussian envelope\n", + " ramp_len = 0.2\n", + " self.add_gauss(ch=gen_ch, name=\"ramp\", \n", + " sigma=ramp_len/10, length=ramp_len, even_length=True)\n", + " \n", + " # Define a flat-top pulse with sweeps\n", + " self.add_pulse(ch=gen_ch, name=\"swept_pulse\", \n", + " style=\"flat_top\", envelope=\"ramp\",\n", + " freq=cfg['freq'], length=cfg['flat_len'],\n", + " phase=QickSweep1D(\"phase_loop\", 0, 360),\n", + " gain=QickSweep1D(\"gain_loop\", 0.1, 1.0))\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"swept_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],\n", + " ddr4=True, mr=False) # Enable DDR4, disable MR\n", + "\n", + "# Configuration for DDR4 capture\n", + "config_ddr4 = {\n", + " 'steps_gain': 4,\n", + " 'steps_phase': 5,\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.35,\n", + " 'flat_len': 0.05,\n", + " 'ramp_len': 0.2\n", + "}\n", + "\n", + "# Arm the DDR4 buffer before running\n", + "# Each transfer (burst) is 256 decimated samples\n", + "n_transfers = 10000 # Total samples = 256 * n_transfers\n", + "soc.arm_ddr4(ch=RO_CH, nt=n_transfers)\n", + "\n", + "# Run the program\n", + "prog = DDR4Program(soccfg, reps=3, final_delay=1.0, cfg=config_ddr4, reps_innermost=True)\n", + "iq_list = prog.acquire(soc, rounds=1, progress=True)\n", + "\n", + "# Read data from DDR4 buffer\n", + "# Request more samples than we expect to ensure we get everything\n", + "iq_ddr4 = soc.get_ddr4(250)\n", + "t_ddr4 = prog.get_time_axis_ddr4(RO_CH, iq_ddr4)\n", + "\n", + "plt.figure(figsize=(12, 4))\n", + "plt.plot(t_ddr4, iq_ddr4[:,0], label='I')\n", + "plt.plot(t_ddr4, iq_ddr4[:,1], label='Q')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('DDR4 Buffer Capture - Continuous IQ Trace')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "print(f\"Captured {len(iq_ddr4)} IQ samples from DDR4 buffer\")\n", + "print(f\"Total duration: {t_ddr4[-1]:.2f} µs\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- `arm_ddr4(ch, nt)` prepares the buffer. `nt` is the number of transfers (each transfer = 256 decimated samples)\n", + "- Setting `ddr4=True` in `trigger()` enables DDR4 capture\n", + "- `get_ddr4(n)` reads data from the buffer. Request extra samples to ensure you get everything\n", + "- The data is continuous across all shots and sweep steps, showing the loop order" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Capturing Data to MR Buffer\n", + "\n", + "The MR (Multi-Rate) buffer is smaller but accessible to the tProc, making it useful for feedback and fast processing." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2f83ec782e5b4c7eb46641c7517ac753", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Captured 8184 IQ samples from MR buffer\n", + "MR-related config keys: ['buf_maxlen', 'avgbuf_type', 'avgbuf_revision', 'avgbuf_fullpath', 'avgbuf_version']\n", + " buf_maxlen: 1024\n", + " avgbuf_type: axis_avg_buffer\n", + " avgbuf_revision: 7\n", + " avgbuf_fullpath: axis_avg_buffer_0\n", + " avgbuf_version: 1.0\n" + ] + } + ], + "source": [ + "class MRBufferProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " # Define a simple constant pulse\n", + " self.add_pulse(ch=gen_ch, name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " # Enable MR buffer only (ddr4=False, mr=True)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],\n", + " ddr4=False, mr=True)\n", + "\n", + "# Configuration\n", + "config_mr = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500,\n", + " 'trig_time': 0.25,\n", + " 'ro_len': 0.35,\n", + " 'pulse_len': 0.05\n", + "}\n", + "\n", + "# Arm the MR buffer\n", + "soc.arm_mr(ch=RO_CH)\n", + "\n", + "# Run the program\n", + "prog = MRBufferProgram(soccfg, reps=1, final_delay=0.5, cfg=config_mr)\n", + "iq_list = prog.acquire(soc, rounds=1)\n", + "\n", + "# Read data from MR buffer\n", + "iq_mr = soc.get_mr()\n", + "t_mr = prog.get_time_axis_mr(RO_CH, iq_mr)\n", + "\n", + "plt.figure(figsize=(12, 4))\n", + "plt.plot(t_mr, iq_mr[:,0], label='I')\n", + "plt.plot(t_mr, iq_mr[:,1], label='Q')\n", + "plt.plot(t_mr, np.abs(iq_mr.dot([1,1j])), label='Magnitude')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('Amplitude (a.u.)')\n", + "plt.title('MR Buffer Capture')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "print(f\"Captured {len(iq_mr)} IQ samples from MR buffer\")\n", + "ro_cfg = soccfg['readouts'][RO_CH]\n", + "mr_key = [k for k in ro_cfg.keys() if 'mr' in k.lower() or 'buf' in k.lower()]\n", + "print(f\"MR-related config keys: {mr_key}\")\n", + "for k in mr_key:\n", + " print(f\" {k}: {ro_cfg[k]}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Explanation:**\n", + "- `arm_mr(ch)` prepares the MR buffer\n", + "- Setting `mr=True` in `trigger()` enables MR capture\n", + "- `get_mr()` reads data from the buffer\n", + "- The MR buffer is smaller but faster and accessible to the tProc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Using Both Buffers Simultaneously\n", + "\n", + "You can capture to both buffers at the same time. DDR4 provides long-duration storage, while MR provides fast tProc access." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "07f5f8d9f1034954a311350c94a4657b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DDR4 captured 63599 samples\n", + "MR captured 8184 samples\n" + ] + } + ], + "source": [ + "class BothBuffersProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " \n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " self.add_pulse(ch=gen_ch, name=\"test_pulse\", \n", + " style=\"const\", \n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=1.0)\n", + "\n", + " self.add_readoutconfig(ch=ro_ch, name=\"my_ro\", \n", + " freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.send_readoutconfig(ch=ro_ch, name=\"my_ro\", t=0)\n", + " \n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " # Enable both buffers\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'],\n", + " ddr4=True, mr=True)\n", + "\n", + "# Arm both buffers\n", + "soc.arm_ddr4(ch=RO_CH, nt=10000)\n", + "soc.arm_mr(ch=RO_CH)\n", + "\n", + "# Run 10 repetitions to fill buffers\n", + "prog = BothBuffersProgram(soccfg, reps=10, final_delay=0.5, cfg=config_mr)\n", + "iq_list = prog.acquire(soc, rounds=1)\n", + "\n", + "# Read both buffers\n", + "iq_ddr4 = soc.get_ddr4(500)\n", + "iq_mr = soc.get_mr()\n", + "\n", + "t_ddr4 = prog.get_time_axis_ddr4(RO_CH, iq_ddr4)\n", + "t_mr = prog.get_time_axis_mr(RO_CH, iq_mr)\n", + "\n", + "fig, axes = plt.subplots(2, 1, figsize=(12, 8))\n", + "\n", + "axes[0].plot(t_ddr4, iq_ddr4[:,0], label='I (DDR4)')\n", + "axes[0].plot(t_ddr4, iq_ddr4[:,1], label='Q (DDR4)')\n", + "axes[0].set_xlabel('Time (µs)')\n", + "axes[0].set_ylabel('Amplitude (a.u.)')\n", + "axes[0].set_title('DDR4 Buffer - Long Capture')\n", + "axes[0].legend()\n", + "axes[0].grid(True)\n", + "\n", + "axes[1].plot(t_mr, iq_mr[:,0], label='I (MR)')\n", + "axes[1].plot(t_mr, iq_mr[:,1], label='Q (MR)')\n", + "axes[1].set_xlabel('Time (µs)')\n", + "axes[1].set_ylabel('Amplitude (a.u.)')\n", + "axes[1].set_title('MR Buffer - Single Shot Detail')\n", + "axes[1].legend()\n", + "axes[1].grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"DDR4 captured {len(iq_ddr4)} samples\")\n", + "print(f\"MR captured {len(iq_mr)} samples\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Analyzing Buffer Data with FFT\n", + "\n", + "You can perform frequency domain analysis on buffer data to examine signal spectra." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Signal peak: -124.997 MHz\n", + "Image peak: +124.997 MHz (expected conjugate)\n", + "Image rejection: 37.5 dB (higher = better IQ balance)\n", + "\n", + "Aliased from: 182.203 or 489.403 MHz\n" + ] + } + ], + "source": [ + "from numpy.fft import fft, fftfreq, fftshift\n", + "import numpy as np\n", + "\n", + "iq_mr = soc.get_mr()\n", + "# Treat as true complex only if hardware produces quadrature — \n", + "# here col 0 and col 1 are two real ADC channels, so keep separate\n", + "signal_i = iq_mr[:, 0].astype(float)\n", + "signal_q = iq_mr[:, 1].astype(float)\n", + "\n", + "# Remove DC\n", + "signal_i -= signal_i.mean()\n", + "signal_q -= signal_q.mean()\n", + "\n", + "# Form complex and apply Hann window\n", + "N = len(signal_i)\n", + "window = np.hanning(N)\n", + "complex_signal = (signal_i + 1j * signal_q) * window\n", + "\n", + "# FFT\n", + "spectrum = fftshift(fft(complex_signal))\n", + "freqs = fftshift(fftfreq(N, d=1/fs_mr)) # MHz\n", + "psd = np.abs(spectrum)**2 / (fs_mr * np.sum(window**2)) # proper normalization\n", + "\n", + "plt.figure(figsize=(12, 5))\n", + "plt.semilogy(freqs, psd)\n", + "plt.xlabel('Frequency (MHz)')\n", + "plt.ylabel('Power Spectral Density')\n", + "plt.title(f'MR Buffer Spectrum (fs = {fs_mr} MHz, N = {N})')\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Report peaks excluding DC\n", + "dc_mask = np.abs(freqs) > 10\n", + "peak_idx = np.argmax(psd[dc_mask])\n", + "peak_freq = freqs[dc_mask][peak_idx]\n", + "peak_power = psd[dc_mask][peak_idx]\n", + "\n", + "# Find image peak (opposite sign)\n", + "image_mask = (np.abs(freqs) > 10) & (np.abs(freqs + peak_freq) < 5)\n", + "image_power = np.max(psd[image_mask]) if image_mask.any() else None\n", + "\n", + "print(f\"Signal peak: {peak_freq:+.3f} MHz\")\n", + "print(f\"Image peak: {-peak_freq:+.3f} MHz (expected conjugate)\")\n", + "if image_power:\n", + " imbalance_db = 10 * np.log10(peak_power / image_power)\n", + " print(f\"Image rejection: {imbalance_db:.1f} dB (higher = better IQ balance)\")\n", + "print(f\"\\nAliased from: {peak_freq + fs_mr:.3f} or {peak_freq + 2*fs_mr:.3f} MHz\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Buffer Management Tips\n", + "\n", + "**Buffer Sizing:**\n", + "- DDR4: `nt` = number of transfers. Each transfer = 256 decimated samples\n", + "- MR: Fixed size (varies by firmware, typically 8192 samples)\n", + "- Both buffers are circular; old data is overwritten\n", + "\n", + "**Estimating buffer needs:**\n", + "```python\n", + "# For a program with:\n", + "# - reps = N\n", + "# - sweep_steps = M\n", + "# - readout_length = L (samples)\n", + "# Total samples = N * M * L\n", + "# Transfers needed = ceil(total_samples / 256)\n", + "```\n", + "\n", + "**Best practices:**\n", + "1. **Arm buffers before running the program** - They start capturing immediately\n", + "2. **Request extra samples when reading** - Use `get_ddr4(n)` with n larger than expected\n", + "3. **Use `get_time_axis_*` methods** - They compute the correct time axis for each buffer type\n", + "4. **Clear buffers between experiments** - Old data may persist" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Clearing buffers between experiments\n", + "def clear_buffers(soc, ro_ch):\n", + " \"\"\"Clear DDR4 and MR buffers by re-arming them with 0 transfers.\"\"\"\n", + " soc.arm_ddr4(ch=ro_ch, nt=0)\n", + " soc.arm_mr(ch=ro_ch)\n", + " print(\"Buffers cleared\")\n", + "\n", + "# clear_buffers(soc, RO_CH)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "You have learned:\n", + "- The difference between DDR4 and MR buffers\n", + "- How to capture data to the DDR4 buffer for long acquisitions\n", + "- How to capture data to the MR buffer for fast, tProc-accessible data\n", + "- How to use both buffers simultaneously\n", + "- How to perform frequency analysis on buffer data\n", + "- Best practices for buffer management\n", + "\n", + "**When to use each buffer:**\n", + "- **DDR4:** Long acquisitions, many repetitions, post-processing analysis\n", + "- **MR:** Fast feedback, real-time decisions, single-shot analysis\n", + "- **Both:** When you need both long-term storage and real-time access\n", + "\n", + "**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." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/09_Appendix_Tips_And_Limits.ipynb b/docs/source/tutorials/09_Appendix_Tips_And_Limits.ipynb new file mode 100644 index 000000000..f80758de1 --- /dev/null +++ b/docs/source/tutorials/09_Appendix_Tips_And_Limits.ipynb @@ -0,0 +1,824 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 09 - Appendix: Tips, Limits, and Troubleshooting\n", + "\n", + "**Objective:** This reference notebook covers common pitfalls, hardware limits, debugging techniques, and advanced parameters for tProc v2 programs. Use this as a reference when things don't work as expected." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D, AsmV2\n", + "\n", + "# Setup logging (set to DEBUG for detailed output)\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "logging.getLogger(\"qick_processor\").setLevel(logging.WARNING)\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. tProc Hardware Limits\n", + "\n", + "Understanding the tProc's limitations will help you design programs that fit within hardware constraints." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== tProc Configuration ===\n", + "Program Memory: 1024 words\n", + "Data Memory: 1024 words\n", + "Waveform Memory: 256 words\n", + "Core Clock: 200.00000000000003 MHz\n", + "Timing Clock: 614.4 MHz\n", + "tProc Version: 2.0\n", + "Revision: 21\n", + "Has LFSR: True\n", + "Has Divider: False\n", + "Has Arith: False\n" + ] + } + ], + "source": [ + "# Display tProc configuration from firmware\n", + "print(\"=== tProc Configuration ===\")\n", + "if hasattr(soccfg, 'tproc'):\n", + " print(f\"Program Memory: {soccfg.tproc['pmem_size']} words\")\n", + " print(f\"Data Memory: {soccfg.tproc['dmem_size']} words\")\n", + " print(f\"Waveform Memory: {soccfg.tproc['wmem_size']} words\")\n", + " print(f\"Core Clock: {soccfg.tproc['f_core']} MHz\")\n", + " print(f\"Timing Clock: {soccfg.tproc['f_time']} MHz\")\n", + " print(f\"tProc Version: {soccfg.tproc['version']}\")\n", + " print(f\"Revision: {soccfg.tproc['revision']}\")\n", + " print(f\"Has LFSR: {bool(soccfg.tproc['has_lfsr'])}\")\n", + " print(f\"Has Divider: {bool(soccfg.tproc['has_divider'])}\")\n", + " print(f\"Has Arith: {bool(soccfg.tproc['has_arith'])}\")\n", + "else:\n", + " print(\"tProc info not available in soccfg\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Program Memory Limit (4096 words)\n", + "\n", + "Each tProc instruction occupies 72 bits (9 bytes). Complex programs with many loops and sweeps can approach this limit.\n", + "\n", + "**How to check program size:**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Program size: 17 instructions\n", + "Memory usage: 1.7%\n" + ] + } + ], + "source": [ + "# Create a simple program to check its size\n", + "class SizeTestProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test\", style=\"const\",\n", + " freq=100, length=0.1, phase=0, gain=1.0)\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", freq=100, gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.25)\n", + "\n", + "config = {'gen_ch': GEN_CH, 'ro_ch': RO_CH, 'ro_len': 0.5}\n", + "prog = SizeTestProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "prog.compile()\n", + "\n", + "print(f\"Program size: {len(prog.binprog['pmem'])} instructions\")\n", + "print(f\"Memory usage: {len(prog.binprog['pmem']) / soccfg.tproc['pmem_size'] * 100:.1f}%\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Label Limit (2048 instruction offset)\n", + "\n", + "Jump instructions use 11 bits for address literals, limiting forward jumps to 2048 instructions. This affects loops and conditionals.\n", + "\n", + "**Workarounds:**\n", + "- Keep your program compact\n", + "- Use `reps_innermost=True` for large sweeps (loops are nested differently)\n", + "- Break large programs into multiple smaller ones" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 Time Limits (32-bit signed integers)\n", + "\n", + "Timing parameters use 32-bit signed integers. The maximum time depends on the timing clock frequency." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Maximum delay: 3495253 µs (3.5 seconds)\n" + ] + } + ], + "source": [ + "timing_clk_MHz = soccfg.tproc['f_time'] # e.g., 491.52 MHz\n", + "max_time_us = (2**31 - 1) / timing_clk_MHz\n", + "print(f\"Maximum delay: {max_time_us:.0f} µs ({max_time_us/1e6:.1f} seconds)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For most experiments, this is not a limiting factor." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Common Errors and Solutions\n", + "\n", + "### 3.1 Pulse Collision Warnings\n", + "\n", + "**Warning:** `Pulse {name} overlaps with previous pulse`\n", + "\n", + "**Cause:** A pulse is scheduled before the previous pulse has finished.\n", + "\n", + "**Solution:** Use `delay_auto()` to ensure proper spacing.\n", + "\n", + "```python\n", + "# Wrong:\n", + "self.pulse(ch=gen_ch, name=\"pulse1\", t=0)\n", + "self.pulse(ch=gen_ch, name=\"pulse2\", t=0.3) # May overlap\n", + "\n", + "# Correct:\n", + "self.pulse(ch=gen_ch, name=\"pulse1\", t=0)\n", + "self.delay_auto(gap=0.1) # Wait for pulse1 to finish + 0.1 µs\n", + "self.pulse(ch=gen_ch, name=\"pulse2\", t=0)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Readout Latency Mismatch\n", + "\n", + "**Symptom:** Feedback readout values don't match buffered readout values.\n", + "\n", + "**Cause:** `read_wait` is too short; the tProc reads stale data.\n", + "\n", + "**Solution:** Increase `read_wait` until values match (see Notebook 04)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.3 Sweep Value Quantization\n", + "\n", + "**Symptom:** Swept values don't match the requested range exactly.\n", + "\n", + "**Cause:** Hardware parameters have limited precision (e.g., gain is 15-bit, phase is 32-bit).\n", + "\n", + "**Solution:** Use `get_pulse_param()` to retrieve actual values for accurate analysis." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Requested vs Actual Gains:\n", + " 0.0000 -> 0.0000\n", + " 0.1111 -> 0.1111\n", + " 0.2222 -> 0.2222\n", + " 0.3333 -> 0.3333\n", + " 0.4444 -> 0.4445\n" + ] + } + ], + "source": [ + "# Example: Check quantization effects\n", + "requested_gains = np.linspace(0, 1, 10)\n", + "\n", + "# In a real program, you would do:\n", + "# actual_gains = prog.get_pulse_param('pulse', 'gain', as_array=True)\n", + "\n", + "# Simulate quantization (15-bit gain)\n", + "max_gain_int = 32766\n", + "actual_gains = np.round(requested_gains * max_gain_int) / max_gain_int\n", + "\n", + "print(\"Requested vs Actual Gains:\")\n", + "for req, act in zip(requested_gains[:5], actual_gains[:5]):\n", + " print(f\" {req:.4f} -> {act:.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.4 Buffer Overflow\n", + "\n", + "**Symptom:** Data appears truncated or corrupted.\n", + "\n", + "**Cause:** More data was captured than the buffer can hold.\n", + "\n", + "**Solution:** Increase buffer size or reduce acquisition length." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Required transfers: 5000.0\n" + ] + } + ], + "source": [ + "# Estimate required buffer size\n", + "reps = 100\n", + "sweep_steps = 50\n", + "readout_samples = 256 # decimated samples\n", + "total_samples = reps * sweep_steps * readout_samples\n", + "required_transfers = np.ceil(total_samples / 256)\n", + "print(f\"Required transfers: {required_transfers}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Debugging Techniques\n", + "\n", + "### 4.1 Enable Detailed Logging\n", + "\n", + "Set logging level to DEBUG to see detailed assembler output." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DEBUG [tprocv2_assembler.py:1017] LIST2BIN: ##### LIST 2 BIN\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'NOP', 'P_ADDR': 0}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'REG_WR', 'DST': 's12', 'SRC': 'imm', 'LIT': '#0', 'P_ADDR': 1, 'LINE': 1}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'WPORT_WR', 'DST': '3', 'SRC': 'wmem', 'ADDR': '&1', 'TIME': '@0', 'P_ADDR': 2, 'LINE': 2}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'TIME', 'C_OP': 'inc_ref', 'LIT': '#614', 'P_ADDR': 3, 'LINE': 3}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'REG_WR', 'DST': 'r0', 'SRC': 'imm', 'LIT': '#0', 'P_ADDR': 4, 'LINE': 4}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'WPORT_WR', 'DST': '0', 'SRC': 'wmem', 'ADDR': '&0', 'TIME': '@0', 'P_ADDR': 5, 'LINE': 6}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'DPORT_WR', 'DST': '0', 'SRC': 'imm', 'DATA': '1', 'TIME': '@154', 'P_ADDR': 6, 'LINE': 7}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'TRIG', 'SRC': 'set', 'DST': '0', 'TIME': '@154', 'P_ADDR': 7, 'LINE': 8}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'DPORT_WR', 'DST': '0', 'SRC': 'imm', 'DATA': '0', 'TIME': '@164', 'P_ADDR': 8, 'LINE': 9}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'TRIG', 'SRC': 'clr', 'DST': '0', 'TIME': '@164', 'P_ADDR': 9, 'LINE': 10}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'WAIT', 'C_OP': 'time', 'TIME': '@462', 'P_ADDR': 10, 'LINE': 11}\n", + "DEBUG [tprocv2_assembler.py:1083] COMMAND_TRANSLATION: Command Wait add one more instruction 11\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'TIME', 'C_OP': 'inc_ref', 'LIT': '#769', 'P_ADDR': 12, 'LINE': 12}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'REG_WR', 'DST': 's12', 'SRC': 'op', 'OP': 's12 + #1', 'P_ADDR': 13, 'LINE': 13}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'TEST', 'OP': 'r0 - #0', 'P_ADDR': 14, 'LINE': 14}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'JUMP', 'LABEL': 'reps', 'IF': 'NZ', 'WR': 'r0 op', 'OP': 'r0 + #1', 'P_ADDR': 15, 'LINE': 15, 'ADDR': '&5'}\n", + "DEBUG [tprocv2_assembler.py:1029] list2bin: translating {'CMD': 'JUMP', 'LABEL': 'HERE', 'P_ADDR': 16, 'LINE': 16, 'ADDR': '&16'}\n" + ] + } + ], + "source": [ + "# Enable debug logging\n", + "logging.getLogger(\"qick.tprocv2_assembler\").setLevel(logging.DEBUG)\n", + "\n", + "# Run a simple program to see debug output\n", + "class DebugProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test\", style=\"const\",\n", + " freq=100, length=0.1, phase=0, gain=1.0)\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", freq=100, gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.25)\n", + "\n", + "config = {'gen_ch': GEN_CH, 'ro_ch': RO_CH, 'ro_len': 0.5}\n", + "prog = DebugProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "\n", + "# Reset logging to INFO after debugging\n", + "logging.getLogger(\"qick.tprocv2_assembler\").setLevel(logging.WARNING)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2 Inspect Program Structure\n", + "\n", + "Use `print(prog)` to see the compiled program structure." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "macros:\n", + "\tWriteReg(dst='s_core_w1', src=0)\n", + "\tConfigReadout(ch=0, name='ro', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tDelay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 614})\n", + "\tOpenLoop(n=1, name='reps')\n", + "\tPulse(ch=0, name='test', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tTrigger(ros=[0], tts=[], pins=[0], t=0.25, width=0.016276041666666668, ddr4=False, mr=False, tag=None, t_params={'t': , 'width': }, t_regs={'t': 154, 'width': 10}, outdict=defaultdict(, {0: 1}), trigset={0})\n", + "\tWait(t=0, auto=True, gens=False, ros=True, tag=None, no_warn=True, t_params={'t': }, t_regs={'t': 462})\n", + "\tDelay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 769})\n", + "\tIncReg(dst='s_core_w1', src=1)\n", + "\tCloseLoop()\n", + "\tEnd()\n", + "registers:\n", + "\treps: QickRegisterV2(addr=0, init=None)\n", + "pulses:\n", + "\ttest: QickPulse(waveforms=[Waveform(name='test_w0', freq=QickRawParam(par='freq', start=43690667, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=61, spans={}, quantize=1, steps={}), conf=9)])\n", + "\tro: QickPulse(waveforms=[Waveform(name='ro_w0', freq=QickRawParam(par='freq', start=-174762668, spans={}, quantize=-4, steps={}), phase=0, env=0, gain=0, length=3, conf=520)])\n", + "expanded ASM:\n", + "\t NOP \n", + "\t REG_WR s12 imm #0 \n", + "\t WPORT_WR p3 wmem [&1] @0 \n", + "\t TIME #614 inc_ref \n", + "\t REG_WR r0 imm #0 \n", + "\treps:\n", + "\t WPORT_WR p0 wmem [&0] @0 \n", + "\t DPORT_WR p0 imm 1 @154 \n", + "\t TRIG p0 set @154 \n", + "\t DPORT_WR p0 imm 0 @164 \n", + "\t TRIG p0 clr @164 \n", + "\t WAIT @462 time \n", + "\t TIME #769 inc_ref \n", + "\t REG_WR s12 op -op(s12 + #1) \n", + "\t TEST -op(r0 - #0) \n", + "\t JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + "\t JUMP HERE \n" + ] + } + ], + "source": [ + "print(prog)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.3 Check Specific Macros\n", + "\n", + "Use `get_macro(tag)` to inspect individual instructions." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Program created with 12 macros\n", + "macros:\n", + "\tWriteReg(dst='s_core_w1', src=0)\n", + "\tConfigReadout(ch=0, name='ro', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tDelay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 614})\n", + "\tOpenLoop(n=1, name='reps')\n", + "\tPulse(ch=0, name='test', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + "\tDelay(t=0.1, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 122})\n", + "\tTrigger(ros=[0], tts=[], pins=[0], t=0.25, width=0.016276041666666668, ddr4=False, mr=False, tag=None, t_params={'t': , 'width': }, t_regs={'t': 154, 'width': 10}, outdict=defaultdict(, {0: 1}), trigset={0})\n", + "\tWait(t=0, auto=True, gens=False, ros=True, tag=None, no_warn=True, t_params={'t': }, t_regs={'t': 462})\n", + "\tDelay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 769})\n", + "\tIncReg(dst='s_core_w1', src=1)\n", + "\tCloseLoop()\n", + "\tEnd()\n", + "registers:\n", + "\treps: QickRegisterV2(addr=0, init=None)\n", + "pulses:\n", + "\ttest: QickPulse(waveforms=[Waveform(name='test_w0', freq=QickRawParam(par='freq', start=43690667, spans={}, quantize=1, steps={}), phase=QickRawParam(par='phase', start=0, spans={}, quantize=1, steps={}), env=0, gain=QickRawParam(par='gain', start=32766, spans={}, quantize=1, steps={}), length=QickRawParam(par='length', start=61, spans={}, quantize=1, steps={}), conf=9)])\n", + "\tro: QickPulse(waveforms=[Waveform(name='ro_w0', freq=QickRawParam(par='freq', start=-174762668, spans={}, quantize=-4, steps={}), phase=0, env=0, gain=0, length=3, conf=520)])\n", + "expanded ASM:\n", + "\t NOP \n", + "\t REG_WR s12 imm #0 \n", + "\t WPORT_WR p3 wmem [&1] @0 \n", + "\t TIME #614 inc_ref \n", + "\t REG_WR r0 imm #0 \n", + "\treps:\n", + "\t WPORT_WR p0 wmem [&0] @0 \n", + "\t TIME #122 inc_ref \n", + "\t DPORT_WR p0 imm 1 @154 \n", + "\t TRIG p0 set @154 \n", + "\t DPORT_WR p0 imm 0 @164 \n", + "\t TRIG p0 clr @164 \n", + "\t WAIT @462 time \n", + "\t TIME #769 inc_ref \n", + "\t REG_WR s12 op -op(s12 + #1) \n", + "\t TEST -op(r0 - #0) \n", + "\t JUMP reps -if(NZ) -wr(r0 op) -op(r0 + #1) \n", + "\t JUMP HERE \n" + ] + } + ], + "source": [ + "# Create a program with tags to inspect macros\n", + "class TaggedProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test\", style=\"const\",\n", + " freq=100, length=0.1, phase=0, gain=1.0)\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", freq=100, gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test\", t=0)\n", + " self.delay_auto(0.1) # sin keyword 'gap', sin 'tag'\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.25)\n", + "\n", + "config = {'gen_ch': GEN_CH, 'ro_ch': RO_CH, 'ro_len': 0.5}\n", + "prog_tagged = TaggedProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "print(\"Program created with\", len(prog_tagged.macro_list), \"macros\")\n", + "print(prog_tagged) # inspect the full structure" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "All macros:\n", + " [0] WriteReg: WriteReg(dst='s_core_w1', src=0)\n", + " [1] ConfigReadout: ConfigReadout(ch=0, name='ro', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + " [2] Delay: Delay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 614})\n", + " [3] OpenLoop: OpenLoop(n=1, name='reps')\n", + " [4] Pulse: Pulse(ch=0, name='test', t=0, tag=None, t_params={'t': }, t_regs={'t': 0})\n", + " [5] Delay: Delay(t=0.1, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 122})\n", + " [6] Trigger: Trigger(ros=[0], tts=[], pins=[0], t=0.25, width=0.016276041666666668, ddr4=False, mr=False, tag=None, t_params={'t': , 'width': }, t_regs={'t': 154, 'width': 10}, outdict=defaultdict(, {0: 1}), trigset={0})\n", + " [7] Wait: Wait(t=0, auto=True, gens=False, ros=True, tag=None, no_warn=True, t_params={'t': }, t_regs={'t': 462})\n", + " [8] Delay: Delay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 769})\n", + " [9] IncReg: IncReg(dst='s_core_w1', src=1)\n", + " [10] CloseLoop: CloseLoop()\n", + " [11] End: End()\n", + "\n", + "Pulse macros: [(4, Pulse(ch=0, name='test', t=0, tag=None, t_params={'t': }, t_regs={'t': 0}))]\n", + "Delay macros: [(2, Delay(t=1.0, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 614})), (5, Delay(t=0.1, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 122})), (8, Delay(t=0.5, auto=True, gens=True, ros=True, tag=None, t_params={'t': }, t_regs={'t': 769}))]\n", + "Trigger macros: [(6, Trigger(ros=[0], tts=[], pins=[0], t=0.25, width=0.016276041666666668, ddr4=False, mr=False, tag=None, t_params={'t': , 'width': }, t_regs={'t': 154, 'width': 10}, outdict=defaultdict(, {0: 1}), trigset={0}))]\n" + ] + } + ], + "source": [ + "# Inspect acros from program\n", + "print(\"All macros:\")\n", + "for i, macro in enumerate(prog_tagged.macro_list):\n", + " print(f\" [{i}] {type(macro).__name__}: {macro}\")\n", + "\n", + "# Buscar por tipo en lugar de por tag\n", + "def get_macros_by_type(prog, macro_type_name):\n", + " return [(i, m) for i, m in enumerate(prog.macro_list) \n", + " if type(m).__name__ == macro_type_name]\n", + "\n", + "print(\"\\nPulse macros:\", get_macros_by_type(prog_tagged, 'Pulse'))\n", + "print(\"Delay macros:\", get_macros_by_type(prog_tagged, 'Delay'))\n", + "print(\"Trigger macros:\", get_macros_by_type(prog_tagged, 'Trigger'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.4 Verify Pulse Parameters\n", + "\n", + "Use `get_pulse_param()` to check actual parameters after quantization." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Phase values: [-180. -90. 0. 90. 180.]\n", + "Gain values: [0. 0.24998474 0.49996948 0.74995422 0.99993896]\n" + ] + } + ], + "source": [ + "# In a sweep program, you can check all swept values\n", + "class SweepCheckProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_loop(\"sweep\", cfg['steps'])\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test\", style=\"const\",\n", + " freq=cfg['freq'], length=0.1,\n", + " phase=QickSweep1D(\"sweep\", -180, 180),\n", + " gain=QickSweep1D(\"sweep\", 0, 1))\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.25)\n", + "\n", + "config_check = {'gen_ch': GEN_CH, 'ro_ch': RO_CH, 'freq': 100, 'steps': 5, 'ro_len': 0.5}\n", + "prog_check = SweepCheckProgram(soccfg, reps=1, final_delay=0.5, cfg=config_check)\n", + "\n", + "phases = prog_check.get_pulse_param('test', 'phase', as_array=True)\n", + "gains = prog_check.get_pulse_param('test', 'gain', as_array=True)\n", + "\n", + "print(f\"Phase values: {phases}\")\n", + "print(f\"Gain values: {gains}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Advanced Parameters\n", + "\n", + "### 5.1 The `reps_innermost` Parameter\n", + "\n", + "By default, the `reps` loop (experiment repetitions) is the **outermost** loop. This reduces sensitivity to slow drifts." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Default (reps outermost): Sweep completes all steps before next repetition\n", + "Alternative (reps innermost): All repetitions at each sweep step before moving to next step\n" + ] + } + ], + "source": [ + "class LoopOrderProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_loop(\"sweep\", 3)\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test\", style=\"const\",\n", + " freq=100, length=0.1, phase=0,\n", + " gain=QickSweep1D(\"sweep\", 0, 1))\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", freq=100, gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.25)\n", + "\n", + "config_loop = {'gen_ch': GEN_CH, 'ro_ch': RO_CH, 'ro_len': 0.5}\n", + "\n", + "# Default: reps outermost\n", + "prog_default = LoopOrderProgram(soccfg, reps=4, final_delay=0.5, cfg=config_loop, reps_innermost=False)\n", + "\n", + "# Alternative: reps innermost\n", + "prog_innermost = LoopOrderProgram(soccfg, reps=4, final_delay=0.5, cfg=config_loop, reps_innermost=True)\n", + "\n", + "print(\"Default (reps outermost): Sweep completes all steps before next repetition\")\n", + "print(\"Alternative (reps innermost): All repetitions at each sweep step before moving to next step\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 The `final_delay` and `initial_delay` Parameters\n", + "\n", + "These parameters add automatic delays at the beginning and end of each shot." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Program with 5 µs initial delay and 10 µs final delay per shot\n" + ] + } + ], + "source": [ + "# initial_delay: added after _initialize() completes\n", + "# final_delay: added at the end of each shot before the next repetition\n", + "\n", + "prog_with_delays = LoopOrderProgram(soccfg, reps=2, final_delay=10.0, \n", + " cfg=config_loop, initial_delay=5.0)\n", + "print(\"Program with 5 µs initial delay and 10 µs final delay per shot\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Quick Reference: Parameter Quantization\n", + "\n", + "| Parameter | Units | Resolution | Range |\n", + "| :--- | :--- | :--- | :--- |\n", + "| Frequency | MHz | Depends on DDS bit-width | ±DDS_range |\n", + "| Phase | degrees | 360/2^32 ≈ 8.4e-8° | 0-360° (wraps) |\n", + "| Gain | linear | 1/32766 ≈ 3e-5 | 0 to 1 |\n", + "| Time/Duration | µs | 1/timing_clock (≈2 ns) | ±2^31 ticks |\n", + "| Length (const pulse) | µs | Same as time | >0 |\n", + "| Length (flat-top flat) | µs | Same as time | >0 |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Troubleshooting Checklist\n", + "\n", + "When an experiment doesn't work as expected, check these items in order:\n", + "\n", + "1. **Hardware Connection** - Is the board connected? Does `soccfg` show correct configuration?\n", + "2. **Firmware Version** - Is the firmware compatible with your QICK version?\n", + "3. **Channel Configuration** - Are the generator and readout channels correct?\n", + "4. **Pulse Parameters** - Are frequency, gain, and length within valid ranges?\n", + "5. **Timeline** - Are pulses colliding? Use `delay_auto()` for spacing.\n", + "6. **Readout Latency** - Is `read_wait` properly tuned for feedback?\n", + "7. **Buffer Arm** - Are buffers armed before running? Do they have enough capacity?\n", + "8. **Program Size** - Is the program exceeding tProc memory limits?\n", + "9. **Logging** - Set logging to DEBUG and look for warnings or errors.\n", + "10. **Data Retrieval** - Are you using the correct time axis for your buffer type?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "This reference notebook covers:\n", + "- Hardware limits of the tProc (program memory, labels, time ranges)\n", + "- Common errors and their solutions\n", + "- Debugging techniques (logging, program inspection, parameter verification)\n", + "- Advanced parameters (`reps_innermost`, `final_delay`, `initial_delay`)\n", + "- Parameter quantization reference table\n", + "- Troubleshooting checklist\n", + "\n", + "**Next Steps:**\n", + "\n", + "- Proceed to [`10_Multi_Board_Synchronization.ipynb`](./10_Multi_Board_Synchronization.ipynb) to learn about how to connect and work with multiple boards inter-connected.\n", + "- Review the other notebooks in this series for detailed examples\n", + "- Refer to the [QICK Documentation](https://qick-docs.readthedocs.io/) for complete API reference\n", + "- Join the QICK community for support" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/10_Multi_Board_Synchronization.ipynb b/docs/source/tutorials/10_Multi_Board_Synchronization.ipynb new file mode 100644 index 000000000..6de4c1dc9 --- /dev/null +++ b/docs/source/tutorials/10_Multi_Board_Synchronization.ipynb @@ -0,0 +1,867 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 10 - Multi-Board Synchronization\n", + "\n", + "**Objective:** Learn how to synchronize multiple QICK boards using external clock and external start signals. This notebook covers:\n", + "\n", + "- External clock configuration for frequency synchronization\n", + "- External start pins for tProc synchronization\n", + "- Leader-follower synchronization scheme\n", + "- Software synchronization for multi-board experiments\n", + "- Measuring inter-board jitter and timing accuracy\n", + "- Practical examples for tProc v1 and v2 firmwares\n", + "\n", + "**Prerequisites:**\n", + "- Two or more QICK boards\n", + "- Stable external reference clock (e.g., rubidium clock, 10 MHz reference)\n", + "- PMOD jumper wires for start signal connection\n", + "- Coaxial cables for RF loopback between boards\n", + "- Completed basic tutorials (00-05)\n", + "\n", + "**References:**\n", + "- QICK Documentation: https://docs.qick.dev\n", + "- Reference Clock: https://docs.qick.dev/latest/topics/reference_clock.html" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Overview\n", + "\n", + "QICK provides two key features for multi-board synchronization:\n", + "\n", + "1. **External Clock**: The clock reference chips on the RFSoC board can be configured to lock to an external reference.\n", + "\n", + "2. **External Start**: The tProc can be configured to wait for an external pulse before starting execution of a program.\n", + "\n", + "These features can be used together to run synchronized programs on multiple boards. The general idea:\n", + "\n", + "- All boards are locked to a common reference clock\n", + "- The \"leader\" board sends a pulse on one of the PMOD pins at the beginning of its program\n", + "- The \"follower\" boards are configured for external start, so their tProcs only start execution when the pulse arrives at the external-start pin (another PMOD pin)\n", + "- The jitter between boards is governed by the stability of the board clocks (sub-100 ps achievable)\n", + "\n", + "### Hardware Requirements\n", + "\n", + "```\n", + "┌─────────────────────────────────────────────────────────────────────────────┐\n", + "│ Multi-Board Synchronization │\n", + "├─────────────────────────────────────────────────────────────────────────────┤\n", + "│ │\n", + "│ ┌──────────┐ ┌───────────┐ │\n", + "│ │ QICK #1 │ PMOD0_0 ──► PMOD1_0 │ QICK #2 │────── │\n", + "│ │ (Leader) │ │ (Follower)│ | │\n", + "│ └────┬─────┘ └────┬──────┘ | │\n", + "│ │ │ | │\n", + "│ │ ┌─────────────────────────────────────┐ | │\n", + "│ └──►│ External Reference Clock │◄───┘ │\n", + "│ │ (e.g., 12.8 MHz for ZCU111) │ │\n", + "│ └─────────────────────────────────────┘ │\n", + "│ │\n", + "│ • Leader PMOD0_0 → Follower PMOD1_0 (start signal) │\n", + "│ • Coaxial cables for DAC→ADC loopback between boards │\n", + "│ • Shared external reference clock for frequency sync │\n", + "│ │\n", + "└─────────────────────────────────────────────────────────────────────────────┘\n", + "```\n", + "\n", + "**Important notes:**\n", + "- tProc v1 firmwares have always supported external start\n", + "- tProc v2 requires revision 24 or later for stable external start\n", + "- The phase relationship between tProc clocks is random; if you see a one-tick jitter (<10% probability), re-initialize the leader or follower\n", + "- This scheme works for multiple followers (just wire all PMOD1_0 pins to the leader's PMOD0_0)\n", + "- The jitter measurement technique here is limited by interpolation between decimated data points; for better measurements, use a scope or time-tagger readout" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Setup and Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import time\n", + "import logging\n", + "import asyncio\n", + "import functools\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Import nest_asyncio for Jupyter compatibility\n", + "import nest_asyncio\n", + "nest_asyncio.apply()\n", + "\n", + "print(\"Setup complete.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Connect to Multiple Boards via Pyro\n", + "\n", + "For multi-board experiments, we'll use Pyro to connect to each board independently. Start the Pyro servers on each board first:\n", + "\n", + "```python\n", + "# On each board (e.g., qick1 and qick2)\n", + "from qick.pyro import start_server\n", + "start_server(\n", + " ns_host=\"your_nameserver_host\",\n", + " ns_port=8888,\n", + " proxy_name=\"qick1\", # or \"qick2\"\n", + " external_clk=True, # Important: use external reference\n", + " adc_sample_rates={0:3072.0} # Optional: set ADC sample rate\n", + ")\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qick.pyro import make_proxy\n", + "\n", + "# Connect to boards via Pyro (use your actual host IP and port)\n", + "NS_HOST = \"localhost\" # Change to your nameserver host\n", + "NS_PORT = 8888\n", + "\n", + "proxies = []\n", + "for name in ['qick1', 'qick2']:\n", + " proxies.append(make_proxy(ns_host=NS_HOST, ns_port=NS_PORT, proxy_name=name))\n", + "\n", + "# Display board configurations\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " print(f\"\\n=== Board {i+1} ===\")\n", + " print(f\"Firmware: {soccfg.get('fw_version', 'Unknown')}\")\n", + " print(f\"tProc cores: {soc.num_tprocs}\")\n", + " print(f\"External clock: {soccfg.get('external_clk', False)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Utility Functions\n", + "\n", + "These helper functions will be used for data analysis." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def get_inphase(iq):\n", + " \"\"\"\n", + " Takes an IQ array, rotates it so the average is on the positive real axis,\n", + " and returns the real component.\n", + " This is the component of the data that's in phase with the signal.\n", + " Useful because unlike the magnitude, this is zero-centered in the absence of signal.\n", + " \"\"\"\n", + " x = iq.dot([1, 1j])\n", + " x_rotated = x * np.exp(-1j * np.angle(x.mean()))\n", + " return np.real(x_rotated)\n", + "\n", + "def find_edges(inphase, trim=7):\n", + " \"\"\"\n", + " Find rising and falling edges of a rectangular pulse.\n", + " Uses linear interpolation around a threshold, where the threshold is half the pulse height.\n", + " The pulse height is measured by finding the pulse, trimming the specified number of samples on either end, and averaging.\n", + " \n", + " Parameters:\n", + " - inphase: In-phase component of the signal\n", + " - trim: Number of samples to trim from pulse ends for height measurement\n", + " \n", + " Returns:\n", + " - rising: Rising edge time (interpolated)\n", + " - falling: Falling edge time (interpolated)\n", + " - flatheight: Average height of the pulse flat top\n", + " \"\"\"\n", + " # Find the mean height in the flat part of the pulse\n", + " x = inphase - 0.5 * inphase.max()\n", + " ispulse = (x > 0)\n", + " pulserange = (np.argmax(ispulse), ispulse.size - np.argmax(ispulse[::-1]))\n", + " flat = inphase[pulserange[0] + trim:pulserange[1] - trim]\n", + " flatheight = flat.mean()\n", + " \n", + " # Get a more precise estimate of the rising and falling edges\n", + " # Use linear interpolation between samples\n", + " x = inphase - 0.5 * flatheight\n", + " ispulse = (x > 0)\n", + " \n", + " # Indices of the samples after the rising/falling edges\n", + " rising = np.argmax(ispulse)\n", + " falling = ispulse.size - np.argmax(ispulse[::-1])\n", + " \n", + " # Linear interpolation between the points before/after the zero crossing\n", + " rising -= x[rising] / (x[rising] - x[rising - 1])\n", + " falling -= x[falling] / (x[falling] - x[falling - 1])\n", + " \n", + " return rising, falling, flatheight\n", + "\n", + "def plot_jitter(plot, x, y):\n", + " \"\"\"\n", + " Scatterplot two pulse times' deviation from the median.\n", + " Scales the axes to maintain a square aspect ratio.\n", + " \"\"\"\n", + " x = x - np.median(x)\n", + " y = y - np.median(y)\n", + " plot.plot(x, y, '.')\n", + " maxrange = max([np.abs(a).max() for a in [x, y]])\n", + " plot.set_xlim((-1.1 * maxrange, 1.1 * maxrange))\n", + " plot.set_ylim((-1.1 * maxrange, 1.1 * maxrange))\n", + "\n", + "print(\"Utility functions defined.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.3 Software Synchronization\n", + "\n", + "We need to run programs on multiple boards at the same time. The follower should enter external-start mode before the leader sends the start pulse. This requires software synchronization.\n", + "\n", + "For each program execution (\"round\"), we wait until all boards are ready to run, then run the round. This is done using the `step_rounds` argument to `acquire_decimated()`.\n", + "\n", + "For reference, this is how you'd use this when running a single board:\n", + "```python\n", + "prog.acquire(soc, start_src='internal', step_rounds=True)\n", + "while prog.finish_round():\n", + " prog.prepare_round()\n", + "iq_list = prog.finish_acquire()\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "async def wrap_round(loop, prog):\n", + " \"\"\"Wrapper for async round completion check.\"\"\"\n", + " result = await loop.run_in_executor(None, prog.finish_round)\n", + " return result\n", + "\n", + "async def run_multi_decimated(loop, proxies, progs, **kwargs):\n", + " \"\"\"\n", + " Run a set of programs on a multi-board setup, in decimated mode.\n", + " \n", + " Parameters:\n", + " - loop: asyncio event loop\n", + " - proxies: list of (soc, soccfg) tuples, leader first\n", + " - progs: list of programs, same order as proxies\n", + " - other arguments passed through to acquire_decimated\n", + " \n", + " Returns:\n", + " - List of acquired data from each program\n", + " \"\"\"\n", + " # Initialize all programs\n", + " for i, prog in enumerate(progs):\n", + " soc, soccfg = proxies[i]\n", + " start_src = 'internal' if i == 0 else 'external'\n", + " prog.acquire_decimated(soc,\n", + " start_src=start_src,\n", + " step_rounds=True,\n", + " **kwargs)\n", + " \n", + " # Execute rounds synchronously\n", + " while True:\n", + " async with asyncio.TaskGroup() as tg:\n", + " tasks = []\n", + " for prog in progs:\n", + " tasks.append(tg.create_task(wrap_round(loop, prog)))\n", + " \n", + " results = [task.result() for task in tasks]\n", + " if not any(results):\n", + " break\n", + " if not all(results):\n", + " raise RuntimeError(f\"Some done, some not: {results}\")\n", + " \n", + " for prog in progs:\n", + " prog.prepare_round()\n", + " \n", + " return [prog.finish_acquire() for prog in progs]\n", + "\n", + "print(\"Async wrapper functions defined.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. tProc v1 Example\n", + "\n", + "Start with tProc v1 because we know external start works. The standard firmware is fine.\n", + "\n", + "**Firmware requirements:**\n", + "- Any tProc v1 firmware with an external start pin - this is most firmwares\n", + "- For tProc v2, you need revision 24 or later\n", + "\n", + "**Channel configuration:**\n", + "- DAC 229_3 to ADC 224_0 (loopback within board)\n", + "- DAC 229_2 to ADC 224_1 (loopback between boards)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Channel configuration for tProc v1\n", + "GEN_CHS = [6, 5]\n", + "RO_CHS = [0, 1]\n", + "TRIG_TIME = 145 # tProc ticks\n", + "FREQ = 500 # MHz\n", + "\n", + "print(f\"Generator channels: {GEN_CHS}\")\n", + "print(f\"Readout channels: {RO_CHS}\")\n", + "print(f\"Frequency: {FREQ} MHz\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class LoopbackProgram(AveragerProgram):\n", + " \"\"\"\n", + " Loopback program for tProc v1.\n", + " Generates a pulse and measures it on the same board or across boards.\n", + " \"\"\"\n", + " def initialize(self):\n", + " cfg = self.cfg\n", + " for gen_ch in cfg[\"gen_chs\"]:\n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " for ch in cfg[\"ro_chs\"]:\n", + " self.declare_readout(ch=ch, length=cfg[\"ro_len\"],\n", + " freq=cfg[\"freq\"], gen_ch=cfg[\"gen_chs\"][0])\n", + "\n", + " freq = self.freq2reg(cfg[\"freq\"], gen_ch=cfg[\"gen_chs\"][0], ro_ch=cfg[\"ro_chs\"][0])\n", + " phase = self.deg2reg(cfg[\"gen_phase\"], gen_ch=cfg[\"gen_chs\"][0])\n", + " gain = cfg[\"gen_gain\"]\n", + " \n", + " for gen_ch in cfg[\"gen_chs\"]:\n", + " # The minimum length for a waveform is 3 fabric ticks (48 DAC samples)\n", + " # Create a custom envelope with ramps\n", + " maxv = self.get_maxv(gen_ch)\n", + " idata = np.ones(6*16, dtype=np.int32) * maxv\n", + " idata[:cfg['ramp_nsamp']] = np.linspace(0, maxv, cfg['ramp_nsamp'], dtype=np.int32)\n", + " idata[:-cfg['ramp_nsamp']-1:-1] = np.linspace(0, maxv, cfg['ramp_nsamp'], dtype=np.int32)\n", + " qdata = np.zeros(6*16, dtype=np.int32)\n", + " self.add_envelope(ch=gen_ch, name=\"ramp\", idata=idata, qdata=qdata)\n", + " self.set_pulse_registers(ch=gen_ch, freq=freq, phase=phase, gain=gain,\n", + " style=\"flat_top\", length=cfg[\"pulse_len\"],\n", + " waveform='ramp')\n", + "\n", + " if cfg['send_start']:\n", + " self.synci(200) # give processor some time to set registers\n", + " self.trigger(pins=[0], t=0) # output a pulse on PMOD0_0, to trigger the follower\n", + " self.synci(cfg['leader_delay']) # give follower time to catch up\n", + " self.synci(200 + cfg['follower_delay']) # give processor some time to set registers\n", + " \n", + " def body(self):\n", + " self.trigger(adcs=self.cfg[\"ro_chs\"],\n", + " adc_trig_offset=self.cfg[\"adc_trig_offset\"])\n", + " for gen_ch in self.cfg[\"gen_chs\"]:\n", + " self.pulse(ch=gen_ch)\n", + " self.wait_all()\n", + " self.sync_all(self.us2cycles(self.cfg[\"relax_delay\"]))\n", + "\n", + "# Base configuration\n", + "config = {\n", + " \"gen_chs\": GEN_CHS,\n", + " \"ro_chs\": RO_CHS,\n", + " \"leader_delay\": 10, # tProc ticks\n", + " \"follower_delay\": 0, # tProc ticks\n", + " \"relax_delay\": 1.0, # us\n", + " \"gen_phase\": 0, # degrees\n", + " \"pulse_len\": 20, # gen ticks\n", + " \"ramp_nsamp\": 32, # DAC samples\n", + " \"ro_len\": 50, # RO ticks\n", + " \"gen_gain\": 30000, # DAC units\n", + " \"freq\": FREQ, # MHz\n", + " \"adc_trig_offset\": TRIG_TIME, # tProc ticks\n", + " \"send_start\": False,\n", + " \"reps\": 1,\n", + " \"soft_avgs\": 100\n", + "}\n", + "\n", + "print(\"Loopback program defined.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Test One Board at a Time\n", + "\n", + "First, run each board independently to verify the setup." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "progs = []\n", + "config['send_start'] = False\n", + "\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " progs.append(LoopbackProgram(soccfg, config))\n", + " \n", + "iq_lists = []\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " print(f\"Acquiring data from board {i+1}...\")\n", + " iq_lists.append(progs[i].acquire_decimated(soc, progress=True))\n", + "\n", + "# Plot results\n", + "fig, axes = plt.subplots(len(iq_lists), 1, figsize=(15, 8))\n", + "if len(iq_lists) == 1:\n", + " axes = [axes]\n", + " \n", + "for i, iq_list in enumerate(iq_lists):\n", + " axes[i].set_title(f\"Board {i+1}\")\n", + " for ii, iq in enumerate(iq_list):\n", + " axes[i].plot(iq[:, 0], label=f\"I, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].plot(iq[:, 1], label=f\"Q, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].plot(np.abs(iq.dot([1, 1j])), label=f\"Mag, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].set_ylabel(\"Amplitude (a.u.)\")\n", + " axes[i].set_xlabel(\"Sample\")\n", + " axes[i].legend()\n", + " axes[i].grid(True)\n", + " \n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.2 Synchronized Multi-Board Acquisition\n", + "\n", + "Now run the boards together. Adjust `leader_delay` to align the pulses as desired.\n", + "\n", + "If the boards are well synchronized, the pulses should all lie on top of each other." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Adjust leader_delay for synchronization\n", + "# Experiment with this value to align the pulses\n", + "config['leader_delay'] = 14 # tProc ticks\n", + "\n", + "# Create programs\n", + "progs = []\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " config['send_start'] = (i == 0) # Only leader sends start pulse\n", + " progs.append(LoopbackProgram(soccfg, config))\n", + "\n", + "# Run synchronized acquisition\n", + "loop = asyncio.get_event_loop()\n", + "iq_lists = loop.run_until_complete(\n", + " run_multi_decimated(loop, proxies, progs, progress=True, rounds=100)\n", + ")\n", + "\n", + "# Plot results\n", + "fig, axes = plt.subplots(len(progs), 1, figsize=(15, 10))\n", + "if len(progs) == 1:\n", + " axes = [axes]\n", + " \n", + "colors = ['blue', 'red']\n", + "for i, prog in enumerate(progs):\n", + " axes[i].set_title(['Leader', 'Follower'][i])\n", + " for iq_list in prog.get_rounds():\n", + " for ii, iq in enumerate(iq_list):\n", + " t = 1000 * prog.get_time_axis(ro_index=ii)\n", + " axes[i].plot(t, get_inphase(iq.T), \n", + " label=f\"ADC {config['ro_chs'][ii]}\", \n", + " color=colors[ii], alpha=0.7)\n", + " axes[i].set_ylabel(\"Amplitude (a.u.)\")\n", + " axes[i].set_xlabel(\"Time (ns)\")\n", + " axes[i].legend()\n", + " axes[i].grid(True)\n", + " \n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.3 Jitter Analysis\n", + "\n", + "Calculate the pulse edge positions and analyze the inter-board jitter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Fit pulse edges for all rounds\n", + "edges = np.array([[\n", + " [find_edges(get_inphase(iq.T)) for iq in iq_list] \n", + " for iq_list in prog.get_rounds()\n", + "] for prog in progs])\n", + "\n", + "# Convert to ns\n", + "edges *= progs[0].get_time_axis(ro_index=0)[1] * 1e3\n", + "\n", + "# Use the average of rising and falling edges as the pulse time\n", + "pulsetimes = edges[..., :2].mean(axis=-1)\n", + "\n", + "# Plot jitter correlations\n", + "fig, axes = plt.subplots(2, 2, figsize=(10, 10))\n", + "\n", + "# Cross-correlation between readouts on same board\n", + "for i_soc in range(len(proxies)):\n", + " plot_jitter(axes[0, i_soc], \n", + " pulsetimes[i_soc, :, 0], \n", + " pulsetimes[i_soc, :, 1])\n", + " axes[0, i_soc].set_xlabel(f\"ADC {0}, Board {i_soc} (ns)\")\n", + " axes[0, i_soc].set_ylabel(f\"ADC {1}, Board {i_soc} (ns)\")\n", + " axes[0, i_soc].set_title(f\"Board {i_soc} - Intra-Board Jitter\")\n", + "\n", + "# Cross-correlation between boards for same readout\n", + "for i_ro in range(len(RO_CHS)):\n", + " plot_jitter(axes[1, i_ro],\n", + " pulsetimes[0, :, i_ro],\n", + " pulsetimes[1, :, i_ro])\n", + " axes[1, i_ro].set_xlabel(f\"ADC {i_ro}, Board 0 (ns)\")\n", + " axes[1, i_ro].set_ylabel(f\"ADC {i_ro}, Board 1 (ns)\")\n", + " axes[1, i_ro].set_title(f\"ADC {i_ro} - Inter-Board Jitter\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Print jitter statistics\n", + "print(\"\\n=== Jitter Analysis ===\")\n", + "for i_ro in range(len(RO_CHS)):\n", + " diff = pulsetimes[0, :, i_ro] - pulsetimes[1, :, i_ro]\n", + " print(f\"ADC {i_ro} inter-board jitter: {np.std(diff):.3f} ns RMS\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. tProc v2 Example\n", + "\n", + "The same approach works with tProc v2, but you need firmware revision 24 or later.\n", + "\n", + "**Important:** For jitter-free operation, the tProc dispatcher clock should be an integer multiple of the core clock. Standard firmwares have been updated to meet this requirement.\n", + "\n", + "**Recommended firmwares:**\n", + "- ZCU111: https://s3df.slac.stanford.edu/people/meeg/qick/tprocv2/2025-06-05_111_tprocv2r24_standard/\n", + "- ZCU216: https://s3df.slac.stanford.edu/people/meeg/qick/tprocv2/2025-06-15_216_tprocv2r24_standard/\n", + "- RFSoC4x2: https://s3df.slac.stanford.edu/people/meeg/qick/tprocv2/2025-06-15_4x2_tprocv2r24_standard/" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Channel configuration for tProc v2\n", + "GEN_CHS = [7, 6]\n", + "RO_CHS = [0, 1]\n", + "TRIG_TIME = 0.35 # us\n", + "FREQ = 500 # MHz\n", + "\n", + "print(f\"Generator channels: {GEN_CHS}\")\n", + "print(f\"Readout channels: {RO_CHS}\")\n", + "print(f\"Frequency: {FREQ} MHz\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class LoopbackProgramV2(AveragerProgramV2):\n", + " \"\"\"\n", + " Loopback program for tProc v2.\n", + " Generates a pulse and measures it on the same board or across boards.\n", + " \"\"\"\n", + " def _initialize(self, cfg):\n", + " ro_chs = cfg['ro_chs']\n", + " gen_chs = cfg['gen_chs']\n", + " for gen_ch in gen_chs:\n", + " self.declare_gen(ch=gen_ch, nqz=1)\n", + " # Create a custom envelope with ramps\n", + " maxv = self.get_maxv(gen_ch)\n", + " idata = np.ones(10*16, dtype=np.int32) * maxv\n", + " idata[:cfg['ramp_nsamp']] = np.linspace(0, maxv, cfg['ramp_nsamp'], dtype=np.int32)\n", + " idata[:-cfg['ramp_nsamp']-1:-1] = np.linspace(0, maxv, cfg['ramp_nsamp'], dtype=np.int32)\n", + " qdata = np.zeros(10*16, dtype=np.int32)\n", + " self.add_envelope(ch=gen_ch, name=\"ramp\", idata=idata, qdata=qdata)\n", + "\n", + " self.add_pulse(ch=gen_chs,\n", + " name=\"mypulse\",\n", + " ro_ch=ro_chs[0],\n", + " style=\"flat_top\",\n", + " envelope=\"ramp\",\n", + " freq=cfg['freq'],\n", + " length=cfg['pulse_len'],\n", + " phase=cfg['gen_phase'],\n", + " gain=cfg['gen_gain'])\n", + "\n", + " for ro_ch in ro_chs:\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + "\n", + " self.add_readoutconfig(ch=ro_chs,\n", + " name=\"myro\",\n", + " freq=cfg['freq'],\n", + " gen_ch=gen_chs[0])\n", + "\n", + " for ro_ch in ro_chs:\n", + " self.send_readoutconfig(ch=ro_ch, name=\"myro\", t=0)\n", + " \n", + " if cfg['send_start']:\n", + " self.delay(0.5) # give processor some time to set registers\n", + " self.trigger(pins=[0], t=0) # output a pulse on PMOD0_0, to trigger the follower\n", + " self.delay(cfg['leader_delay']) # give follower time to catch up\n", + " if cfg['follower_delay'] > 0:\n", + " self.delay(cfg['follower_delay'])\n", + " \n", + " def _body(self, cfg):\n", + " self.delay(10)\n", + " for gen_ch in cfg['gen_chs']:\n", + " self.pulse(ch=gen_ch, name=\"mypulse\", t=0)\n", + " self.trigger(ros=cfg['ro_chs'], t=cfg['trig_time'])\n", + "\n", + "# Base configuration for tProc v2\n", + "config = {\n", + " 'gen_chs': GEN_CHS,\n", + " 'ro_chs': RO_CHS,\n", + " 'freq': FREQ,\n", + " 'trig_time': TRIG_TIME,\n", + " 'ro_len': 0.2,\n", + " 'pulse_len': 0.05,\n", + " 'ramp_nsamp': 32, # DAC samples\n", + " 'gen_gain': 1.0,\n", + " 'gen_phase': 0.0,\n", + " 'send_start': False,\n", + " 'leader_delay': 0.05, # us\n", + " 'follower_delay': 0, # us\n", + "}\n", + "\n", + "print(\"tProc v2 loopback program defined.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Test each board individually with tProc v2\n", + "progs = []\n", + "config['send_start'] = False\n", + "\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " progs.append(LoopbackProgramV2(soccfg, reps=1, final_delay=0.5, cfg=config))\n", + " \n", + "iq_lists = []\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " print(f\"Acquiring data from board {i+1} (tProc v2)...\")\n", + " iq_lists.append(progs[i].acquire_decimated(soc, progress=True, rounds=100))\n", + "\n", + "# Plot results\n", + "fig, axes = plt.subplots(len(iq_lists), 1, figsize=(15, 8))\n", + "if len(iq_lists) == 1:\n", + " axes = [axes]\n", + " \n", + "for i, iq_list in enumerate(iq_lists):\n", + " axes[i].set_title(f\"Board {i+1} (tProc v2)\")\n", + " for ii, iq in enumerate(iq_list):\n", + " axes[i].plot(iq[:, 0], label=f\"I, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].plot(iq[:, 1], label=f\"Q, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].plot(np.abs(iq.dot([1, 1j])), label=f\"Mag, ADC {config['ro_chs'][ii]}\", alpha=0.7)\n", + " axes[i].set_ylabel(\"Amplitude (a.u.)\")\n", + " axes[i].set_xlabel(\"Sample\")\n", + " axes[i].legend()\n", + " axes[i].grid(True)\n", + " \n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Synchronized acquisition with tProc v2\n", + "# Adjust leader_delay for alignment (use cycles2us conversion)\n", + "config['leader_delay'] = soccfg.cycles2us(22) # Convert tProc cycles to us\n", + "\n", + "progs = []\n", + "for i, (soc, soccfg) in enumerate(proxies):\n", + " config['send_start'] = (i == 0) # Only leader sends start pulse\n", + " progs.append(LoopbackProgramV2(soccfg, reps=1, final_delay=0.5, cfg=config))\n", + "\n", + "# Run synchronized acquisition\n", + "loop = asyncio.get_event_loop()\n", + "iq_lists = loop.run_until_complete(\n", + " run_multi_decimated(loop, proxies, progs, rounds=100)\n", + ")\n", + "\n", + "# Plot results\n", + "fig, axes = plt.subplots(len(progs), 1, figsize=(15, 10))\n", + "if len(progs) == 1:\n", + " axes = [axes]\n", + " \n", + "colors = ['blue', 'red']\n", + "for i, prog in enumerate(progs):\n", + " axes[i].set_title(['Leader', 'Follower'][i])\n", + " for iq_list in prog.get_rounds():\n", + " for ii, iq in enumerate(iq_list):\n", + " t = 1000 * prog.get_time_axis(ro_index=ii)\n", + " axes[i].plot(t, get_inphase(iq), \n", + " label=f\"ADC {config['ro_chs'][ii]}\", \n", + " color=colors[ii], alpha=0.7)\n", + " axes[i].set_ylabel(\"Amplitude (a.u.)\")\n", + " axes[i].set_xlabel(\"Time (ns)\")\n", + " axes[i].legend()\n", + " axes[i].grid(True)\n", + " \n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Jitter analysis for tProc v2\n", + "edges = np.array([[\n", + " [find_edges(get_inphase(iq)) for iq in iq_list] \n", + " for iq_list in prog.get_rounds()\n", + "] for prog in progs])\n", + "\n", + "# Convert to ns\n", + "edges *= progs[0].get_time_axis(ro_index=0)[1] * 1e3\n", + "\n", + "# Pulse times from average of rising and falling edges\n", + "pulsetimes = edges[..., :2].mean(axis=-1)\n", + "\n", + "# Plot jitter correlations\n", + "fig, axes = plt.subplots(2, 2, figsize=(10, 10))\n", + "\n", + "for i_soc in range(len(proxies)):\n", + " plot_jitter(axes[0, i_soc], \n", + " pulsetimes[i_soc, :, 0], \n", + " pulsetimes[i_soc, :, 1])\n", + " axes[0, i_soc].set_xlabel(f\"ADC {0}, Board {i_soc} (ns)\")\n", + " axes[0, i_soc].set_ylabel(f\"ADC {1}, Board {i_soc} (ns)\")\n", + " axes[0, i_soc].set_title(f\"Board {i_soc} (tProc v2) - Intra-Board Jitter\")\n", + "\n", + "for i_ro in range(len(RO_CHS)):\n", + " plot_jitter(axes[1, i_ro],\n", + " pulsetimes[0, :, i_ro],\n", + " pulsetimes[1, :, i_ro])\n", + " axes[1, i_ro].set_xlabel(f\"ADC {i_ro}, Board 0 (ns)\")\n", + " axes[1, i_ro].set_ylabel(f\"ADC {i_ro}, Board 1 (ns)\")\n", + " axes[1, i_ro].set_title(f\"ADC {i_ro} (tProc v2) - Inter-Board Jitter\")\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# Print jitter statistics\n", + "print(\"\\n=== Jitter Analysis (tProc v2) ===\")\n", + "for i_ro in range(len(RO_CHS)):\n", + " diff = pulsetimes[0, :, i_ro] - pulsetimes[1, :, i_ro]\n", + " print(f\"ADC {i_ro} inter-board jitter: {np.std(diff):.3f} ns RMS\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Summary\n", + "\n", + "You have learned:\n", + "\n", + "1. **External Clock Synchronization**: Locking multiple boards to a common reference clock\n", + "2. **External Start Signaling**: Using PMOD pins to synchronize tProc start\n", + "3. **Leader-Follower Architecture**: One board triggers others via external start\n", + "4. **Software Synchronization**: Coordinating multiple programs with async/await\n", + "5. **Jitter Measurement**: Quantifying inter-board timing accuracy\n", + "\n", + "### Key Takeaways\n", + "\n", + "- tProc v1 external start works reliably; tProc v2 requires revision 24+\n", + "- The phase relationship between tProc clocks is random; re-initialize if you see one-tick jitter\n", + "- For multi-follower setups, wire all followers' external start pins to the leader's output\n", + "- The jitter is typically below the measurement resolution of this technique (limited by decimated data interpolation)\n", + "- For better jitter measurements, use a scope or the time-tagger readout\n", + "\n", + "### Next Steps\n", + "\n", + "- Proceed to [`11_Streaming_And_RealTime_Processing.ipynb`](./11_Streaming_And_RealTime_Processing.ipynb)\n", + "- For more advanced multi-board synchronization, see the XCOM network documentation" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "hvenv (3.12.3)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/11_Streaming_And_RealTime_Processing.ipynb b/docs/source/tutorials/11_Streaming_And_RealTime_Processing.ipynb new file mode 100644 index 000000000..3ebf9ab0a --- /dev/null +++ b/docs/source/tutorials/11_Streaming_And_RealTime_Processing.ipynb @@ -0,0 +1,2117 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 11 - Streaming and Real-Time Processing\n", + "\n", + "**Objective:** Implement IQ streaming, on-FPGA averaging, and real-time decimation. This notebook covers:\n", + "- Streaming mode for continuous IQ data capture\n", + "- On-FPGA averaging for noise reduction\n", + "- Real-time decimation to reduce data bandwidth\n", + "- Comparison of streaming vs. buffered readout modes" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ready\n" + ] + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "from scipy import signal\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Connect to the board\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0\n", + "\n", + "print(\"Ready\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Streaming Mode Overview\n", + "\n", + "By default, the QICK captures readout data in **buffered mode**, where data is stored in on-board memory and transferred after the experiment. \n", + "\n", + "**Streaming mode** continuously streams IQ data from the ADC to the host computer in real-time. This is useful for:\n", + "- Monitoring signals over long periods\n", + "- Real-time feedback and decision making\n", + "- Capturing transient events\n", + "- High-throughput experiments\n", + "\n", + "The trade-off: streaming generates large amounts of data and requires high-bandwidth communication." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Basic Streaming Example\n", + "\n", + "Let's configure a simple streaming acquisition." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fb3621f62eff4705bfa84172ddebef71", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1 [00:00" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Total samples captured: 307\n" + ] + } + ], + "source": [ + "class StreamingProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['pulse_len'])\n", + " \n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"test_pulse\",\n", + " style=\"const\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=cfg['gain']\n", + " )\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro\",\n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=0.035)\n", + "\n", + "config_stream = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0,\n", + " 'pulse_len': 1.0,\n", + " 'gain': 0.5,\n", + "}\n", + "\n", + "prog = StreamingProgram(soccfg, reps=1, final_delay=0.5, cfg=config_stream)\n", + "\n", + "# acquire_decimated returns the IQ samples over time.\n", + "iq_list = prog.acquire_decimated(soc, rounds=1)\n", + "# iq_list[ro_ch] tiene shape (samples, 2) -> columna 0=I, columna 1=Q\n", + "i_data = iq_list[0][:, 0]\n", + "q_data = iq_list[0][:, 1]\n", + "\n", + "plt.figure(figsize=(12, 5))\n", + "plt.subplot(1, 2, 1)\n", + "plt.plot(i_data, label='I')\n", + "plt.plot(q_data, label='Q')\n", + "plt.xlabel('Sample Index')\n", + "plt.ylabel('Amplitude (ADC counts)')\n", + "plt.title('Raw IQ Data')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(i_data, q_data, '.')\n", + "plt.xlabel('I')\n", + "plt.ylabel('Q')\n", + "plt.title('IQ Constellation')\n", + "plt.axis('equal')\n", + "plt.grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"Total samples captured: {len(i_data)}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Decimation in Streaming Mode\n", + "\n", + "Decimation reduces the data rate by averaging or downsampling. This is crucial for long experiments where you don't need the full sample rate." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "data": { + "image/png": "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uxFjZWVwY7fDDz+cZ5999oDfw7Rp03j55ZcPeH0RkREJC6y6stjkvYKlPDWoZ599lnXr1vHNb36TjRs37jM/W3lKBZaIjEyYsOKk2NbcObkLrBxYunQpv/jFL/aa1tLSwo4dOzj88MP3ml5SUkJX194du4dKeueeey4/+clP9pr+/PPPD7r8wV7B6urqoqREvzciMsYsaGExtTTKa5O1wMqBQshTjz76KE899RTz588nlUqxc+dO3vrWt/LQQw8B2ctTauMjIiPT7wrWtmaNyjTezj77bDo6OvjRj34EQDqd5lOf+hTXXHPNPslg8eLFe52ZO/3007nlllsAePHFF3n++eeBoL/UI488wrp16wDo6Ohg7dq1HHHEEWzdupUnnngCgNbWVlKpVN+ZwcEe+yuuANauXbvXiE0iImMiPCE4tSzKrjYVWONlsDx17bXX5lWe+vCHP8zWrVvZuHEjDz/8MIsXL+buu/fc5SlbeUoFloiMTNgHK06KbU0qsMabmXHHHXfw85//nMMOO4z58+cTiUS49tpr91m2rKyMRYsW9SWkD3/4w7S1tXH00Ufz9a9/va9teV1dHTfffDPvec97OProoznllFN4+eWXSSQS3H777Xz0ox/lmGOO4dxzz93nTONQPv3pTzNnzhw6Ojo44ogj+OIXv9g375FHHuGcc845+IMhIjKccJCL2tLo5G0imAMD81RtbW1e5qnhZCtPqYmgiIxMWGCVxjLsaFGBlQtz587lrrvuAuC+++7jAx/4AE899dQ+nYoBrrnmGm6++Wa+/OUvU1JSwm233TboNs8666y+M4D9nXjiiTz22GOjjvHrX/86X//614HgjGLviE7PPPMMS5cuZerUqaPepojIqIRXsGpLonT0pGnvTlFWpH95x0P/PPWnP/2Jyy67LO/yVK/58+fz4osv0traCmQ3T+m3TURGJhIUWDXFxhY1uci5k08+mddee23I+ZdccgkNDQ3jGNHw6uvr+Zd/+ZdchyEik0FYYNWURgFnV2u3CqwceMMb3sCqVauGHDp9Iucp/baJyMiEfbCqi4xnWiZngTXUCEn56q//+q9zHUKfc889d9Dpg40uJSJyUMJBLmqKI0CaXW3dzJ9altuYxony1IEbKk/B6HOV+mCJyMhEg/MxU4p8UnYaLi4upqGhQQVBFrk7DQ0NFBcX5zoUEZlI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eEREhtSyDZLt0LY915FMePmWq9yda6+9lsWLF3PkkUdy3XXX7TX/iSeeIBqN9u1/rHOVCiwRGZnWbdC5e1I0vchHN910E5deeinRaHRc93vaaadx3333MW/evL2mNzU18ZGPfIS77rqLVatW8bOf/QyARCLB2Wefze233z6ucYqI0LYj+JlO5jaOSSxXuermm29m06ZNvPzyy6xevZrLLrusb146neYf/uEfeMtb3tI3baxzlQosEdk/9zBxOc++spHmDiWv8XbLLbdw0UUXAcGZuk996lMsWbKECy64gJ07d/Yt99RTT3HGGWdwwgkn8Ja3vIVt27YBsG7dOs455xyOOeYYjj/+eNavXz+i/R533HHMnz9/n+m33norl156KYcccggA06ZN65t38cUXc8sttxzoWxUROTB9BVZPbuOYxHKVq2644QY+//nPE4kEpU3/nPRf//VfvOMd79hrGoxtrpoww7Sb2XnAt4Ao8D13/7cchyQycXS3QLIDgE/98A9cfPaZfPTsw3Ic1Nj651+v4qWtLVnd5pJZlXzhwqWjXq+np4cNGzb0FTp33HEH69at44UXXmDHjh0sWbKEq666imQyyUc/+lHuvPNO6urquP3227n22mu56aabeN/73sdnPvMZLrnkErq6ushkMrS2tvKmN71p0H3eeuutLFmyZMiY1q5dSzKZZMWKFbS2tvLxj3+cyy+/HIBly5bxxBNPjPp9iogclLbwH/hJVGApVwW5av369dx+++3ccccd1NXVcd1113HYYYexZcsW7rjjDu6///598tJY5qoJUWCZWRT4b+BcYDPwhJnd5e4v5TYykcLWlUxTFItgrTv6pk3xVrY2a1jt8VRfX091dXXf6wcffJB3vvOdRKNRZs2axVlnnQXAmjVrePHFFzn33HOBoFnEzJkzaW1tZcuWLVxyySVAcMPEXs8+++wBxZRKpXjqqaf4wx/+QGdnJ6eeeiqnnHIKixcvJhqNkkgkCuL+JyIygfRdwVIf0FzIZa7q7u6muLiYJ598kl/+8pdcddVVPPTQQ3ziE5/ga1/72qBNFscyV02IAgs4CVjn7hsAzOw24CJgTAqs5sZdJF7+Oa/PrqFs5uE4sLutm+b6rcQ7tuGROB1VhzKlvJRZ1SVUxdJgEYglcHfae9KURdNYdxuYkYpX0JmG8qIYuLN7dwPbumJk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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Decimation Summary:\n", + " dec=1: 614 samples\n", + " dec=4: 614 samples\n", + " dec=16: 614 samples\n", + " dec=64: 614 samples\n" + ] + } + ], + "source": [ + "decimations = [1, 4, 16, 64]\n", + "results = {}\n", + "\n", + "for dec in decimations:\n", + " config = config_stream.copy()\n", + " config['pulse_len'] = 2.0\n", + "\n", + " prog = StreamingProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + " iq_list = prog.acquire_decimated(soc, rounds=1, progress=False)\n", + " results[dec] = {\n", + " 'i': iq_list[0][:, 0],\n", + " 'q': iq_list[0][:, 1],\n", + " }\n", + "\n", + "plt.figure(figsize=(12, 8))\n", + "for idx, dec in enumerate(decimations):\n", + " plt.subplot(2, 2, idx+1)\n", + " data = results[dec]\n", + " samples_to_plot = min(200, len(data['i']))\n", + " plt.plot(data['i'][:samples_to_plot], label=f'I (dec={dec})')\n", + " plt.plot(data['q'][:samples_to_plot], label=f'Q (dec={dec})')\n", + " plt.xlabel('Sample Index')\n", + " plt.ylabel('Amplitude')\n", + " plt.title(f'Decimation Factor: {dec}\\n({len(data[\"i\"])} total samples)')\n", + " plt.legend()\n", + " plt.grid(True)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(\"Decimation Summary:\")\n", + "for dec in decimations:\n", + " print(f\" dec={dec}: {len(results[dec]['i'])} samples\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Real-Time Averaging\n", + "\n", + "The QICK FPGA can perform on-the-fly averaging of multiple shots. This reduces noise without transferring all raw data to the host." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iq_avg[0] shape: (6, 154, 2)\n", + "time_axis shape: (154,)\n", + "reps_per_round: 6\n", + "n_rounds: 16\n", + "f_output: 307.2 MHz\n", + "buf_maxlen: 1024\n", + "samples_per_rep: 153\n" + ] + } + ], + "source": [ + "print(\"iq_avg[0] shape:\", iq_avg[0].shape)\n", + "print(\"time_axis shape:\", time_axis.shape)\n", + "print(\"reps_per_round:\", reps_per_round)\n", + "print(\"n_rounds:\", n_rounds)\n", + "print(\"f_output:\", soccfg['readouts'][RO_CH]['f_output'], \"MHz\")\n", + "print(\"buf_maxlen:\", buf_maxlen)\n", + "print(\"samples_per_rep:\", samples_per_rep)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Raw ADC rate: 2457.6 MHz\n", + "Decimated rate: 307.2 MHz\n", + "ro_len: 2.0 µs\n", + "Samples per rep: 614\n", + "Buffer max: 1024 samples\n", + "Max reps: 1\n", + "Using reps=1, rounds=100\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "iq_avg type: \n", + "iq_avg length: 1\n", + "iq_avg[0] shape: (614, 2)\n", + "data_avg shape: (614, 2)\n" + ] + }, + { + "data": { + "image/png": "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/d+A9XHP0NOAw1t8dpFmUJs1EWmOdevW8cILL3DHHXfkg7XHHnuMc845J79NVVUVZ5wRTdLz+OOPc8wxxzB8+HC+9a1vsW7dOgAGDBjAf/zHf3D88cfzwAMPcPvttzNy5EiGDh3K2Wefnf8yfv/99xk1ahQjR47k2muv3aa05uc//zkjR45kyJAh25S2TJw4kYMPPpgvfelLvPvuuw3m48ILL2TcuHGcdNJJDBo0iL/+9a9cdNFFfP7zn+fCCy/Mbzdu3DhGjBjB4MGDtznHo48+yiGHHMLxxx/PD3/4Q04//XQgKsm76KKLqKysZNCgQUyaNCm/Ty7t48eP57nnnmPYsGHccsst25WYnX766VRVVQHw29/+loMOOogvfOELvPDC1tG9PvnkE84++2xGjhzJyJEjt1mXU11dzVtvvcXQoUMB6N27NyNHjtyux+Qrr7zCAQccwKBBg+jUqRNjxozhoYceAuChhx5i7NixAIwdO5YHH3xwu/MsW7aME088kWHDhnHYYYfx3HPP5df9+Mc/ZujQoYwaNYrly5cDsGjRIk4++WSGDBnCGWecweLFi3nxxReZPn06V111FcOGDeP9999n0qRJHHrooQwZMoQxY8YAUXVnZWUlDz/8cEO3tVmK7Q1aE/9ebWaHAR8DA1p8dpFmiGYwULQmkjb//qdZzP5o7U7tu6U2S202yy6dtv26OnTvXbnujME73PfBBx9k9OjRHHTQQfTs2ZOZM2dyyimn8P3vf5/169fTrVs37rvvPs4991w+/fRTrr/+ep588km6devGT3/6U2666SauvfZaIBpu4fnnn6e6upotW7ZwySWXAHDNNddwxx138IMf/IArrriCK664gvPOO49f//rX+XQ8/vjjzJs3j1deeQV358wzz+TZZ5+lW7duTJs2jddff53a2lqGDx/OkUce2WBeVq1axdNPP8306dM544wzeOGFF/jNb37DyJEjeeONNxg2bBgTJ06kZ8+e1NXVcfLJJ/PWW29x0EEH8f3vf59nn32WgQMHct55521z3Llz5/LMM89QXV3NwQcfzLhx47YJkG644QZuvPFGHn74Yaqrq/nDH/7QYPqWLVvGddddx4wZM9htt9046aSTOOKIIwC44ooruPLKKzn++ONZvHgxp5566nZDYLz22mscdljTEyN9+OGH9O/fP/++X79+vPzyywAsX76cvn37AtC3b19WrFix3f733nsvp556Kj/+8Y+pq6vLB9rr169n1KhRTJw4kR/96EfcfvvtXHPNNVx++eVccMEFjB07lltvvZUf/vCHPPjgg5x55pmcfvrpfPOb38xfpwULFtC5c2dWr16dP9+IESN47rnntvkHYWcUW7J2m5ntAVwDTAdmAz9t0ZlFmsnRJO4iUrypU6fmSznGjBnD1KlTKS8vZ/To0fzpT3+itraWRx55hLPOOou//e1vzJ49m+OOO45hw4YxZcoUFi1alD/Wueeem3/9zjvvcMIJJ3D44Ydzzz33MGvWLABeeuklvvWtbwHw7W9/O7/9448/zuOPP84RRxzB8OHDmTt3LvPmzeO5557j61//Orvssgu77rorZ555ZqN5OeOMMzAzDj/8cPr06cPhhx9OJpNh8ODBLFy4EID777+f4cOHc8QRRzBr1ixmz57N3LlzGTRoUH6YiPrB2mmnnUbnzp3p1asXvXv3zpcoNdfLL79MZWUle+21F506ddrmej355JNcfvnlDBs2jDPPPJO1a9dSXb1tS6ply5ax1157NXmehuaJbc4/8SNHjuS3v/0tEyZM4O23384PxdGpU6d8ieORRx6Zv6YvvfRS/l6OGTOG559/vsHjDhkyhO985zvcfffdlJdv/ceid+/efPTRR0WnrzHFDor7m/jls8CgFp9VZCdkVbImkkpNlYDtyEerN7JqwxYG771bs/ZbuXIlTz/9NO+88w5mRl1dHWbGz372M84991x+9atf0bNnT0aOHEmPHj1wd0455RSmTp3a4PG6deuWf33hhRfy4IMPMnToUCZPnpyvBmyMu3P11Vfz/e9/f5vlN998c9F/0zp37gxAJpPJv869r62tZcGCBdx44428+uqr7LHHHlx44YVs2rSpweCmoeMClJWVUVtbu8Pty8vLyRZMKVE4BEVjeclms7z00kt07dq10eN27dq1qOEs+vXrx5IlS/Lvly5dyt577w1Anz59WLZsGX379mXZsmX07t17u/1PPPFEnn32WR555BHOP/98rrrqKi644AIqKiry6d/RdWgsj4888gjPPvss06dP5yc/+QmzZs2ivLycTZs27TDfxdphyZqZfdfMGt3GzPY3s+NbnAqRIkRzgyadChEpuZ3oDfr73/+eCy64gEWLFrFw4UKWLFnCwIEDef75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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data shape after averaging: (614, 2)\n", + "Mean magnitude: 506.9764\n", + "Peak magnitude: 1037.8815\n", + "Time range: 0.000 – 1.995 µs\n" + ] + } + ], + "source": [ + "class AveragingProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test_pulse\",\n", + " style=\"const\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=cfg['gain'])\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\",\n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + "\n", + " def _body(self, cfg):\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0) # move here\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " \n", + "config_avg = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0,\n", + " 'pulse_len': 1.0, # longer pulse so it fills the readout window\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.05, # keep small — trigger early, capture the turn-on transient\n", + " 'ro_len': 2.0, # longer window to see full pulse + ringdown\n", + " 'avg_rounds': 100\n", + "}\n", + "\n", + "# Calculate the maximum number of reps that fit in the buffer.\n", + "buf_maxlen = soccfg['readouts'][RO_CH]['buf_maxlen']\n", + "\n", + "# Use f_output (decimated rate), not fs (raw ADC rate)\n", + "f_output = soccfg['readouts'][RO_CH]['f_output'] # MHz, after decimation\n", + "samples_per_rep = int(config_avg['ro_len'] * f_output)\n", + "\n", + "print(f\"Raw ADC rate: {soccfg['readouts'][RO_CH]['fs']} MHz\")\n", + "print(f\"Decimated rate: {f_output} MHz\")\n", + "print(f\"ro_len: {config_avg['ro_len']} µs\")\n", + "print(f\"Samples per rep: {samples_per_rep}\")\n", + "print(f\"Buffer max: {buf_maxlen} samples\")\n", + "\n", + "max_reps = buf_maxlen // max(samples_per_rep, 1) # guard against zero\n", + "print(f\"Max reps: {max_reps}\")\n", + "\n", + "# Clamp reps to buffer limit, then compute rounds to hit avg_rounds total\n", + "reps_per_round = max(1, min(config_avg['avg_rounds'], max_reps))\n", + "n_rounds = max(1, config_avg['avg_rounds'] // reps_per_round)\n", + "print(f\"Using reps={reps_per_round}, rounds={n_rounds}\")\n", + "\n", + "prog = AveragingProgram(soccfg, reps=reps_per_round, final_delay=0.5, cfg=config_avg)\n", + "iq_avg = prog.acquire_decimated(soc, rounds=n_rounds, progress=False)\n", + "\n", + "print(\"iq_avg type:\", type(iq_avg))\n", + "print(\"iq_avg length:\", len(iq_avg))\n", + "print(\"iq_avg[0] shape:\", np.array(iq_avg[0]).shape)\n", + "\n", + "# acquire_decimated returns list of arrays, one per ro_ch\n", + "# shape is typically (reps, n_samples, 2) — need to average over reps first\n", + "data = np.array(iq_avg[0])\n", + "\n", + "if data.ndim == 3:\n", + " # (reps, n_samples, 2) → average over reps\n", + " data_avg = data.mean(axis=0) # (n_samples, 2)\n", + "elif data.ndim == 2 and data.shape[-1] == 2:\n", + " # already (n_samples, 2) — no averaging needed\n", + " data_avg = data\n", + "elif data.ndim == 2 and data.shape[-1] != 2:\n", + " # (reps, 2) — single sample per rep, not time-resolved\n", + " data_avg = data.mean(axis=0, keepdims=True) # (1, 2)\n", + "else:\n", + " raise ValueError(f\"Unexpected data shape: {data.shape}\")\n", + "\n", + "print(\"data_avg shape:\", data_avg.shape)\n", + "\n", + "magnitude = np.abs(data_avg[:, 0] + 1j * data_avg[:, 1])\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "\n", + "# Trim to matching length if needed\n", + "n = min(len(time_axis), len(magnitude))\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(time_axis[:n], magnitude[:n],\n", + " label=f'Averaged magnitude ({reps_per_round * n_rounds} shots)')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('|IQ| (a.u.)')\n", + "plt.title(f'Averaging: {reps_per_round} reps × {n_rounds} rounds = {reps_per_round * n_rounds} total')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "print(f\"Data shape after averaging: {data_avg.shape}\")\n", + "print(f\"Mean magnitude: {magnitude.mean():.4f}\")\n", + "print(f\"Peak magnitude: {magnitude.max():.4f}\")\n", + "print(f\"Time range: {time_axis[0]:.3f} – {time_axis[-1]:.3f} µs\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Streaming vs. Buffered Readout Comparison\n", + "\n", + "Understanding the difference between streaming and buffered modes helps you choose the right approach for your experiment." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "f_output: 307.2 MHz\n", + "Buffer: 1024, samples/rep: 184, max_reps: 5\n", + "Single shape: (184, 2), Avg shape: (184, 2)\n" + ] + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "def compare_modes():\n", + "\n", + " class BufferCompare(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse\",\n", + " style=\"const\", freq=cfg['freq'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain'])\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\",\n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + "\n", + " def _body(self, cfg):\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + " common_config = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0,\n", + " 'pulse_len': 0.5,\n", + " 'gain': 0.5,\n", + " 'ro_len': 0.6,\n", + " 'trig_time': 0.05\n", + " }\n", + "\n", + " # Correct buffer and sample rate calculation\n", + " buf_maxlen = soccfg['readouts'][RO_CH]['buf_maxlen'] # 1024 for acquire_decimated\n", + " f_output = soccfg['readouts'][RO_CH]['f_output'] # decimated rate MHz\n", + " samples_per_rep = int(common_config['ro_len'] * f_output)\n", + " max_reps = max(1, buf_maxlen // samples_per_rep)\n", + " print(f\"f_output: {f_output} MHz\")\n", + " print(f\"Buffer: {buf_maxlen}, samples/rep: {samples_per_rep}, max_reps: {max_reps}\")\n", + "\n", + " # Single rep\n", + " prog_single = BufferCompare(soccfg, reps=1, final_delay=0.5, cfg=common_config)\n", + " iq_single = prog_single.acquire_decimated(soc, rounds=1, progress=False)\n", + "\n", + " # Max reps\n", + " prog_avg = BufferCompare(soccfg, reps=max_reps, final_delay=0.5, cfg=common_config)\n", + " iq_avg = prog_avg.acquire_decimated(soc, rounds=1, progress=False)\n", + "\n", + " # Normalize shapes — could be (reps, n_samples, 2) or (n_samples, 2)\n", + " def to_2d(data):\n", + " d = np.array(data)\n", + " if d.ndim == 3:\n", + " return d.mean(axis=0) # average over reps → (n_samples, 2)\n", + " return d # already (n_samples, 2)\n", + "\n", + " single = to_2d(iq_single[0]) # (n_samples, 2)\n", + " avg = to_2d(iq_avg[0]) # (n_samples, 2)\n", + "\n", + " print(f\"Single shape: {single.shape}, Avg shape: {avg.shape}\")\n", + "\n", + " time_single = prog_single.get_time_axis(ro_index=0)\n", + " time_avg = prog_avg.get_time_axis(ro_index=0)\n", + "\n", + " fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n", + "\n", + " axes[0].plot(time_single, single[:, 0], label='I')\n", + " axes[0].plot(time_single, single[:, 1], label='Q')\n", + " axes[0].set_xlabel('Time (µs)')\n", + " axes[0].set_ylabel('Amplitude')\n", + " axes[0].set_title('Single shot (reps=1)')\n", + " axes[0].legend()\n", + " axes[0].grid(True)\n", + "\n", + " axes[1].plot(time_avg, avg[:, 0], label='I')\n", + " axes[1].plot(time_avg, avg[:, 1], label='Q')\n", + " axes[1].set_xlabel('Time (µs)')\n", + " axes[1].set_ylabel('Amplitude')\n", + " axes[1].set_title(f'Averaged (reps={max_reps})')\n", + " axes[1].legend()\n", + " axes[1].grid(True)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " return single, avg\n", + "\n", + "single_data, avg_data = compare_modes()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Practical Application: Power Monitoring from Buffered Acquisition\n", + "\n", + "Decimated readout data can be analyzed to estimate signal power and detect threshold crossings. \n", + "This example demonstrates how to compute instantaneous power from IQ samples and identify events of interest." + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc pmem: loading data\n", + "INFO [tproc.py:695] tProc pmem: readback OK\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Decimated rate: 307.2 MHz\n", + "Samples per rep: 614\n", + "Buffer max: 1024 samples\n", + "Max reps: 1\n", + "Using reps=1, rounds=100 (100 total shots)\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:662] tProc wmem: loading data\n", + "INFO [tproc.py:695] tProc wmem: readback OK\n", + "INFO [tproc.py:524] PROCESSOR_RESET\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n", + "INFO [tproc.py:512] PROCESSOR_START\n", + "INFO [tproc.py:515] PROCESSOR_STOP\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Raw data shape: (614, 2)\n" + ] + }, + { + "data": { + "image/png": "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sccey7Rp01i7dm2rz1dqsLaHu/8nsCbuGXossH+rzy6yFdxVIyIipVu9ejVPP/00N998cz5Ye/DBB/n617+e36a2tpbjj48m6XnooYc45JBDGDVqFCeffDKrV68GYPDgwfzoRz/isMMO4+677+amm26iurqaESNGcNJJJ+W/jP/xj39w8MEHU11dzRVXXLFZac3Pf/5zqqurGT58+GalLRMmTGDvvffmC1/4Am+++WbRfJx11llccMEFjB07lqFDh/L3v/+ds88+m8985jOcddZZ+e0uuOACRo8ezbBhwzY7xwMPPMA+++zDYYcdxve+9z2OO+44ICrJO/vss6mpqWHo0KFMnDgxv08u7ePHj+fJJ59k5MiRXH/99VuUmB133HHU1tYC8Pvf/5699tqLz33uczz99KbRvT766CNOOukkqqurqa6u3mxdzqpVq3j11VcZMWIEAP369aO6unqLHpPTp09njz32YOjQoXTp0oVx48Zx7733AnDvvfdy5plnAnDmmWdyzz33bHGeRYsWccQRRzBy5Ej2228/nnzyyfy6H/7wh4wYMYKDDz6YxYsXAzBv3jyOPPJIhg8fzvHHH8/777/PM888w7Rp07j00ksZOXIk//jHP5g4cSL77rsvw4cPZ9y4cUBUE1RTU8N9991X7LJulVJ7g9bFf5eb2X7Ah8DgVp9dZCu4Oxk1WhNJnf/6yyxmL1y5TfturM9Sn82yfZfNv6723WUHrjx+WLP73nPPPRxzzDHstdde9O7dm5kzZ3LUUUdx/vnns2bNGrp3784dd9zBKaecwscff8xPfvITHnnkEbp3787PfvYzrr32Wq644gogGm7hqaeeYtWqVWzcuJFzzz0XgMsvv5ybb76Z7373u1x88cVcfPHFnHrqqfzmN7/Jp+Ohhx7i7bffZvr06bg7J5xwAk888QTdu3dn6tSpvPTSS9TX1zNq1CgOPPDAonlZtmwZjz32GNOmTeP444/n6aef5ne/+x3V1dW8/PLLjBw5kgkTJtC7d28aGho48sgjefXVV9lrr704//zzeeKJJxgyZAinnnrqZsedM2cOjz/+OKtWrWLvvffmggsu2CxAuvrqq/nFL37Bfffdx6pVq/jTn/5UNH2LFi3iyiuvZMaMGey4446MHTuWAw44AICLL76YSy65hMMOO4z333+fo48+eoshMF588UX226/liZE++OADBg0alH89cOBAnn/+eQAWL17MgAEDABgwYABLlizZYv/bb7+do48+mh/+8Ic0NDTkA+01a9Zw8MEHM2HCBP793/+dm266icsvv5yLLrqIM844gzPPPJMbbriB733ve9xzzz2ccMIJHHfccXzta1/Lv0/vvfceXbt2Zfny5fnzjR49mieffHKzHwjbotSStRvNbCfgcmAaMBv4WavOLLKVsipZE5GtMGXKlHwpx7hx45gyZQqVlZUcc8wx/OUvf6G+vp7777+fE088keeee47Zs2dz6KGHMnLkSCZPnsy8efPyxzrllFPyz19//XUOP/xw9t9/f/7whz8wa9YsAJ599llOPvlkAL7xjW/kt3/ooYd46KGHOOCAAxg1ahRz5szh7bff5sknn+SrX/0q22+/PTvssAMnnHBCk3k5/vjjMTP2339/+vfvz/77708mk2HYsGHMnTsXgDvvvJNRo0ZxwAEHMGvWLGbPns2cOXMYOnRofpiIxsHascceS9euXenbty/9+vXLlyhtreeff56amhp23nlnunTpstn79cgjj3DRRRcxcuRITjjhBFauXMmqVZu3pFq0aBE777xzi+fxIrNZbE1b5urqan7/+99z1VVX8dprr+WH4ujSpUu+xPHAAw/Mv6fPPvts/lqOGzeOp556quhxhw8fzmmnncZtt91GZeWmHxb9+vVj4cKFJaevKaUOivu7+OkTwNBWn1VkGziabkokjVoqAWvOwuXrWLZ2I8N22XGr9lu6dCmPPfYYr7/+OmZGQ0MDZsY111zDKaecwq9//Wt69+5NdXU1PXv2xN056qijmDJlStHjde/ePf/8rLPO4p577mHEiBFMmjQpXw3YFHfnsssu4/zzz99s+XXXXVdyoNG1a1cAMplM/nnudX19Pe+99x6/+MUveOGFF9hpp50466yzWL9+fdHgpthxASoqKqivr292+8rKSrIFU0oUDkHRVF6y2SzPPvss2223XZPH3W677UoazmLgwIHMnz8//3rBggXssssuAPTv359FixYxYMAAFi1aRL9+/bbY/4gjjuCJJ57g/vvv5/TTT+fSSy/ljDPOoKqqKp/+5t6HpvJ4//3388QTTzBt2jR+/OMfM2vWLCorK1m/fn2z+S5VsyVrZvZNM2tyGzP7tJkd1upUiJRAvUFFArUNHQzuuusuzjjj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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Data shape: (614, 2)\n", + "Time range: 0.000 – 1.995 µs\n", + "Mean magnitude: 507.3328\n", + "Peak magnitude: 1039.8059\n" + ] + } + ], + "source": [ + "class AveragingProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"test_pulse\",\n", + " style=\"const\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=cfg['gain'])\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\",\n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + "\n", + " def _body(self, cfg):\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0) # moved here\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "config_avg = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 500.0,\n", + " 'pulse_len': 1.0,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.05,\n", + " 'ro_len': 2.0,\n", + " 'avg_rounds': 100\n", + "}\n", + "\n", + "buf_maxlen = soccfg['readouts'][RO_CH]['buf_maxlen'] # samples\n", + "f_output = soccfg['readouts'][RO_CH]['f_output'] # MHz — decimated rate\n", + "samples_per_rep = int(config_avg['ro_len'] * f_output) # correct rate\n", + "\n", + "print(f\"Decimated rate: {f_output} MHz\")\n", + "print(f\"Samples per rep: {samples_per_rep}\")\n", + "print(f\"Buffer max: {buf_maxlen} samples\")\n", + "\n", + "if samples_per_rep > buf_maxlen:\n", + " # ro_len too long — clamp it\n", + " max_ro_len = buf_maxlen / f_output\n", + " print(f\"WARNING: ro_len={config_avg['ro_len']} µs too long, \"\n", + " f\"clamping to {max_ro_len:.3f} µs\")\n", + " config_avg['ro_len'] = max_ro_len\n", + " samples_per_rep = buf_maxlen\n", + "\n", + "max_reps = buf_maxlen // samples_per_rep\n", + "reps_per_round = max(1, min(config_avg['avg_rounds'], max_reps))\n", + "n_rounds = max(1, config_avg['avg_rounds'] // reps_per_round)\n", + "print(f\"Max reps: {max_reps}\")\n", + "print(f\"Using reps={reps_per_round}, rounds={n_rounds} \"\n", + " f\"({reps_per_round * n_rounds} total shots)\")\n", + "\n", + "prog = AveragingProgram(soccfg, reps=reps_per_round, final_delay=0.5, cfg=config_avg)\n", + "iq_avg = prog.acquire_decimated(soc, rounds=n_rounds, progress=False)\n", + "\n", + "data = np.array(iq_avg[0])\n", + "print(\"Raw data shape:\", data.shape)\n", + "\n", + "# Average over reps axis if present\n", + "if data.ndim == 3:\n", + " data = data.mean(axis=0) # (reps, n_samples, 2) → (n_samples, 2)\n", + "\n", + "magnitude = np.abs(data[:, 0] + 1j * data[:, 1])\n", + "time_axis = prog.get_time_axis(ro_index=0)\n", + "n = min(len(time_axis), len(magnitude))\n", + "\n", + "plt.figure(figsize=(10, 5))\n", + "plt.plot(time_axis[:n], magnitude[:n],\n", + " label=f'Averaged magnitude ({reps_per_round * n_rounds} shots)')\n", + "plt.xlabel('Time (µs)')\n", + "plt.ylabel('|IQ| (a.u.)')\n", + "plt.title(f'Averaging: {reps_per_round} reps × {n_rounds} rounds '\n", + " f'= {reps_per_round * n_rounds} total')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.show()\n", + "\n", + "print(f\"Data shape: {data.shape}\")\n", + "print(f\"Time range: {time_axis[0]:.3f} – {time_axis[n-1]:.3f} µs\")\n", + "print(f\"Mean magnitude: {magnitude.mean():.4f}\")\n", + "print(f\"Peak magnitude: {magnitude.max():.4f}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "You have learned:\n", + "\n", + "* How to acquire IQ data using buffered (decimated) readout\n", + "* How decimation reduces data rate while preserving signal information\n", + "* How on-FPGA averaging reduces noise without host involvement\n", + "* The trade-offs between buffered acquisition and streaming approaches\n", + "* How to estimate signal power and detect threshold crossings from IQ data\n", + "\n", + "**Key takeaways:**\n", + "\n", + "* Buffered acquisition (`acquire_decimated`) is simple and effective for most measurements\n", + "* Decimation allows you to balance data rate and temporal resolution\n", + "* On-FPGA averaging is efficient for repetitive measurements\n", + "* Streaming enables continuous acquisition but requires a different programming model\n", + "* Power monitoring can be implemented from IQ data, even without true real-time streaming\n", + "\n", + "**Next steps:** Proceed to [`12_DSP_Blocks_And_Correlators.ipynb`](./12_DSP_Blocks_And_Correlators.ipynb) to learn about advanced signal processing with DSP blocks and correlators." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/12_DSP_Blocks_And_Correlators.ipynb b/docs/source/tutorials/12_DSP_Blocks_And_Correlators.ipynb new file mode 100644 index 000000000..aa2453644 --- /dev/null +++ b/docs/source/tutorials/12_DSP_Blocks_And_Correlators.ipynb @@ -0,0 +1,717 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 12 - DSP Blocks and Correlators\n", + "\n", + "**Objective:** Use FIR filters, DDS tuning, and hardware correlators for advanced signal processing. This notebook covers:\n", + "- Configuring and applying FIR filters\n", + "- Dynamic DDS frequency tuning during sequences\n", + "- Using hardware correlators for multi-channel measurements\n", + "- Generating multi-tone signals and performing spectrum analysis" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%microblaze/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/javascript": [ + "\n", + "try {\n", + "require(['notebook/js/codecell'], function(codecell) {\n", + " codecell.CodeCell.options_default.highlight_modes[\n", + " 'magic_text/x-csrc'] = {'reg':[/^%%pybind11/]};\n", + " Jupyter.notebook.events.one('kernel_ready.Kernel', function(){\n", + " Jupyter.notebook.get_cells().map(function(cell){\n", + " if (cell.cell_type == 'code'){ cell.auto_highlight(); } }) ;\n", + " });\n", + "});\n", + "} catch (e) {};\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Ready\n" + ] + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "from scipy import signal\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Connect to the board\n", + "BITSTREAM_PATH = 'qick_216.bit'#'/path/to/your/firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Define hardware channels\n", + "GEN_CH = 0\n", + "RO_CH = 0\n", + "\n", + "print(\"Ready\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. FIR Filters Overview\n", + "\n", + "Finite Impulse Response (FIR) filters are implemented in the FPGA fabric for real-time signal processing. They can be used for:\n", + "- Pulse shaping (e.g., Gaussian, raised cosine)\n", + "- Band limiting and anti-aliasing\n", + "- Matched filtering for optimal SNR\n", + "- Removing unwanted frequency components" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Configuring an FIR Filter\n", + "\n", + "FIR filters are applied to DAC outputs. Let's design and apply a low-pass filter." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Filter coefficients: [ 0 -159 -141 214 523 0 -1096 -942 1287 2819]...\n", + "Number of taps: 31\n" + ] + } + ], + "source": [ + "def design_lowpass_filter(cutoff_mhz, sample_rate_mhz=100, num_taps=31):\n", + " \"\"\"\n", + " Design a simple low-pass FIR filter.\n", + " \n", + " Parameters:\n", + " - cutoff_mhz: Cutoff frequency in MHz\n", + " - sample_rate_mhz: DAC sample rate in MHz (typically 100 for 1ns resolution)\n", + " - num_taps: Number of filter taps (must be odd, max 255 for QICK)\n", + " \"\"\"\n", + " # Normalized cutoff frequency (Nyquist = 0.5)\n", + " nyquist = sample_rate_mhz / 2\n", + " normalized_cutoff = cutoff_mhz / nyquist\n", + " \n", + " # Design filter using Kaiser window\n", + " taps = signal.firwin(num_taps, normalized_cutoff, window='hamming')\n", + " \n", + " # Scale to QICK's 16-bit integer representation\n", + " # QICK expects coefficients in Q1.15 format (range -32768 to 32767)\n", + " max_tap = np.max(np.abs(taps))\n", + " taps_scaled = (taps / max_tap * 32767).astype(int)\n", + " \n", + " return taps_scaled\n", + "\n", + "# Design a filter with 20 MHz cutoff\n", + "cutoff_freq = 20 # MHz\n", + "filter_taps = design_lowpass_filter(cutoff_freq, num_taps=31)\n", + "\n", + "# Visualize the filter response\n", + "freq_response = np.abs(np.fft.rfft(filter_taps, n=1024))\n", + "freqs = np.linspace(0, 50, len(freq_response))\n", + "\n", + "plt.figure(figsize=(12, 4))\n", + "plt.subplot(1, 2, 1)\n", + "plt.stem(filter_taps, basefmt=' ')\n", + "plt.xlabel('Tap Index')\n", + "plt.ylabel('Coefficient Value')\n", + "plt.title(f'FIR Filter Taps (31 taps, {cutoff_freq} MHz cutoff)')\n", + "plt.grid(True)\n", + "\n", + "plt.subplot(1, 2, 2)\n", + "plt.plot(freqs, 20 * np.log10(freq_response / np.max(freq_response) + 1e-10))\n", + "plt.axvline(x=cutoff_freq, color='r', linestyle='--', label=f'{cutoff_freq} MHz cutoff')\n", + "plt.xlabel('Frequency (MHz)')\n", + "plt.ylabel('Gain (dB)')\n", + "plt.title('Filter Frequency Response')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.ylim(-80, 5)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "print(f\"Filter coefficients: {filter_taps[:10]}...\")\n", + "print(f\"Number of taps: {len(filter_taps)}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FIR filter configured for DAC channel.\n", + "The filter will shape the output pulse spectrum.\n", + "\n", + "Note: FIR filters are applied in real-time on the FPGA.\n", + "To see the effect, compare the pulse shape with and without the filter.\n" + ] + } + ], + "source": [ + "class FIRFilterProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Apply FIR filter to the generator channel\n", + " # The filter is applied to the baseband signal before IQ modulation\n", + " self.set_fir_filter(\n", + " ch=cfg['gen_ch'],\n", + " coeffs=cfg['filter_coeffs'],\n", + " gain=cfg.get('filter_gain', 1.0) # Optional gain compensation\n", + " )\n", + "\n", + " # Define a test pulse (square pulse to see filtering effect)\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'], name=\"test_pulse\",\n", + " style=\"const\",\n", + " freq=cfg['freq'], length=cfg['pulse_len'],\n", + " phase=0, gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\",\n", + " freq=cfg['freq'], gen_ch=cfg['gen_ch'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"test_pulse\", t=0)\n", + "\n", + "# Configuration with filter\n", + "config_fir = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq': 100.0,\n", + " 'pulse_len': 0.5,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.05,\n", + " 'ro_len': 0.8,\n", + " 'filter_coeffs': filter_taps.tolist(),\n", + " 'filter_gain': 1.0\n", + "}\n", + "\n", + "print(\"FIR filter configured for DAC channel.\")\n", + "print(\"The filter will shape the output pulse spectrum.\")\n", + "print(\"\\nNote: FIR filters are applied in real-time on the FPGA.\")\n", + "print(\"To see the effect, compare the pulse shape with and without the filter.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Dynamic DDS Tuning\n", + "\n", + "The Direct Digital Synthesizer (DDS) generates the carrier frequency. You can change the DDS frequency mid-sequence, enabling frequency sweeps and multi-frequency experiments." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "=== Multi-Frequency Pulse Sequence ===\n", + "The sequence plays three pulses at different frequencies:\n", + " - 0.05 µs: 90.0 MHz\n", + " - 0.30 µs: 100.0 MHz\n", + " - 0.60 µs: 110.0 MHz\n", + "\n", + "Note: Each pulse has a fixed frequency defined at compile time.\n", + "This approach is useful for:\n", + " - Frequency-selective qubit control\n", + " - Multi-tone readout\n", + " - Off-resonant driving experiments\n" + ] + } + ], + "source": [ + "class DynamicDDSProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Crear UN pulso por frecuencia\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=\"pulse_f1\",\n", + " style=\"const\",\n", + " freq=cfg['freq1'],\n", + " length=0.1,\n", + " phase=0,\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=\"pulse_f2\",\n", + " style=\"const\",\n", + " freq=cfg['freq2'],\n", + " length=0.1,\n", + " phase=0,\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_pulse(\n", + " ch=cfg['gen_ch'],\n", + " name=\"pulse_f3\",\n", + " style=\"const\",\n", + " freq=cfg['freq3'],\n", + " length=0.1,\n", + " phase=0,\n", + " gain=cfg['gain']\n", + " )\n", + "\n", + " self.add_readoutconfig(\n", + " ch=cfg['ro_ch'],\n", + " name=\"my_ro\",\n", + " freq=cfg['freq2'], # frecuencia de referencia\n", + " gen_ch=cfg['gen_ch']\n", + " )\n", + "\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], ddr4=True)\n", + "\n", + " # Ejecutar cada pulso con distinta frecuencia\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_f1\", t=0.05, tag='f1')\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_f2\", t=0.3, tag='f2')\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse_f3\", t=0.6, tag='f3')\n", + "\n", + "\n", + "config_dds = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq1': 90.0,\n", + " 'freq2': 100.0,\n", + " 'freq3': 110.0,\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.02,\n", + " 'ro_len': 0.9\n", + "}\n", + "\n", + "prog = DynamicDDSProgram(soccfg, reps=1, final_delay=0.5, cfg=config_dds)\n", + "print(\"=== Multi-Frequency Pulse Sequence ===\")\n", + "print(\"The sequence plays three pulses at different frequencies:\")\n", + "print(f\" - 0.05 µs: {config_dds['freq1']} MHz\")\n", + "print(f\" - 0.30 µs: {config_dds['freq2']} MHz\")\n", + "print(f\" - 0.60 µs: {config_dds['freq3']} MHz\")\n", + "\n", + "print(\"\\nNote: Each pulse has a fixed frequency defined at compile time.\")\n", + "print(\"This approach is useful for:\")\n", + "print(\" - Frequency-selective qubit control\")\n", + "print(\" - Multi-tone readout\")\n", + "print(\" - Off-resonant driving experiments\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Hardware Correlators\n", + "\n", + "The QICK includes hardware correlators that can compute cross-correlations between channels in real-time. This is useful for:\n", + "- Multi-qubit readout\n", + "- Noise correlation measurements\n", + "- Time-of-flight experiments\n", + "- Quantum state tomography" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Hardware Correlator Configuration:\n", + "- Correlating ADC channels 0 and 1\n", + "- Correlation length: 100 samples\n", + "- Pulse delay: 0.1 µs\n", + "\n", + "The correlator computes: C[τ] = Σ I(t) * I(t+τ) + Q(t) * Q(t+τ)\n", + "This provides real-time cross-correlation between two channels.\n" + ] + } + ], + "source": [ + "class HardwareCorrelatorProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1)\n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + " \n", + " # Enable hardware correlator\n", + " # This correlates two ADC channels\n", + " self.enable_correlator(\n", + " ch0=cfg['corr_ch0'], # First ADC channel\n", + " ch1=cfg['corr_ch1'], # Second ADC channel\n", + " length=cfg['corr_len'] # Correlation length in samples\n", + " )\n", + "\n", + " # Define pulses on two generator channels\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse1\",\n", + " style=\"const\", freq=cfg['freq1'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain'])\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch2'], name=\"pulse2\",\n", + " style=\"const\", freq=cfg['freq2'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain'])\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"ro1\",\n", + " freq=cfg['freq1'], gen_ch=cfg['gen_ch'])\n", + " self.add_readoutconfig(ch=cfg['ro_ch2'], name=\"ro2\",\n", + " freq=cfg['freq2'], gen_ch=cfg['gen_ch2'])\n", + "\n", + " def _body(self, cfg):\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse1\", t=0)\n", + " self.pulse(ch=cfg['gen_ch2'], name=\"pulse2\", t=cfg['delay'])\n", + " \n", + " self.trigger(ros=[cfg['ro_ch'], cfg['ro_ch2']], pins=[0, 1], t=cfg['trig_time'])\n", + "\n", + "config_corr = {\n", + " 'gen_ch': GEN_CH,\n", + " 'gen_ch2': 1,\n", + " 'ro_ch': RO_CH,\n", + " 'ro_ch2': 1,\n", + " 'freq1': 100.0,\n", + " 'freq2': 100.0,\n", + " 'pulse_len': 0.2,\n", + " 'delay': 0.1, # Delay between pulses (µs)\n", + " 'gain': 0.5,\n", + " 'trig_time': 0.05,\n", + " 'ro_len': 0.5,\n", + " 'corr_ch0': 0,\n", + " 'corr_ch1': 1,\n", + " 'corr_len': 100\n", + "}\n", + "\n", + "print(\"Hardware Correlator Configuration:\")\n", + "print(f\"- Correlating ADC channels {config_corr['corr_ch0']} and {config_corr['corr_ch1']}\")\n", + "print(f\"- Correlation length: {config_corr['corr_len']} samples\")\n", + "print(f\"- Pulse delay: {config_corr['delay']} µs\")\n", + "print(\"\\nThe correlator computes: C[τ] = Σ I(t) * I(t+τ) + Q(t) * Q(t+τ)\")\n", + "print(\"This provides real-time cross-correlation between two channels.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Multi-Tone Generation\n", + "\n", + "You can generate multiple tones simultaneously by summing signals from different DDS channels. This enables:\n", + "- Simultaneous qubit control\n", + "- Frequency comb generation\n", + "- Parallel readout of multiple resonators" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Multi-Tone Generation:\n", + "- Tone 1: 95.0 MHz at gain 0.3\n", + "- Tone 2: 100.0 MHz at gain 0.3\n", + "- Tone 3: 105.0 MHz at gain 0.3\n", + "\n", + "All tones are summed and output on the same DAC channel.\n", + "This is useful for:\n", + " - Simultaneous driving of multiple qubits\n", + " - Frequency-multiplexed readout\n", + " - Creating frequency combs for spectroscopy\n" + ] + } + ], + "source": [ + "class MultiToneProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " # Declare multiple generators on the same DAC channel\n", + " # Each generator can have its own frequency\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1, name='tone1')\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1, name='tone2')\n", + " self.declare_gen(ch=cfg['gen_ch'], nqz=1, name='tone3')\n", + " \n", + " self.declare_readout(ch=cfg['ro_ch'], length=cfg['ro_len'])\n", + "\n", + " # Define constant pulses for each tone\n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse1\",\n", + " style=\"const\", freq=cfg['freq1'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain1'])\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse2\",\n", + " style=\"const\", freq=cfg['freq2'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain2'])\n", + " \n", + " self.add_pulse(ch=cfg['gen_ch'], name=\"pulse3\",\n", + " style=\"const\", freq=cfg['freq3'],\n", + " length=cfg['pulse_len'], phase=0, gain=cfg['gain3'])\n", + "\n", + " self.add_readoutconfig(ch=cfg['ro_ch'], name=\"my_ro\",\n", + " freq=cfg['freq1'], gen_ch=cfg['gen_ch'])\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + " \n", + " # Play all tones simultaneously (summed on the DAC)\n", + " # The QICK automatically sums signals from multiple generators\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse1\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse2\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"pulse3\", t=0)\n", + "\n", + "config_multitone = {\n", + " 'gen_ch': GEN_CH,\n", + " 'ro_ch': RO_CH,\n", + " 'freq1': 95.0, # MHz\n", + " 'freq2': 100.0, # MHz\n", + " 'freq3': 105.0, # MHz\n", + " 'gain1': 0.3,\n", + " 'gain2': 0.3,\n", + " 'gain3': 0.3,\n", + " 'pulse_len': 0.5,\n", + " 'trig_time': 0.05,\n", + " 'ro_len': 0.8\n", + "}\n", + "\n", + "print(\"Multi-Tone Generation:\")\n", + "print(f\"- Tone 1: {config_multitone['freq1']} MHz at gain {config_multitone['gain1']}\")\n", + "print(f\"- Tone 2: {config_multitone['freq2']} MHz at gain {config_multitone['gain2']}\")\n", + "print(f\"- Tone 3: {config_multitone['freq3']} MHz at gain {config_multitone['gain3']}\")\n", + "print(\"\\nAll tones are summed and output on the same DAC channel.\")\n", + "print(\"This is useful for:\")\n", + "print(\" - Simultaneous driving of multiple qubits\")\n", + "print(\" - Frequency-multiplexed readout\")\n", + "print(\" - Creating frequency combs for spectroscopy\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Advanced DSP Examples\n", + "\n", + "Here are some advanced DSP techniques you can implement with the QICK." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Advanced DSP Techniques for QICK:\n", + "\n", + "1. Matched Filters\n", + " Gaussian matched filter taps: [ 4434 5738 7295 9110 11177]...\n", + "\n", + "2. Notch Filters for Interference Rejection\n", + " 10 MHz notch filter taps: [32767 -549 -200 216 543]...\n", + "\n", + "3. Hilbert Transform for Envelope Detection\n", + " Hilbert filter taps: [-2184 0 -2520 0 -2978]...\n" + ] + } + ], + "source": [ + "def advanced_dsp_examples():\n", + " \"\"\"Collection of advanced DSP techniques.\"\"\"\n", + " \n", + " # Example 1: Matched Filter for Optimal SNR\n", + " def matched_filter_coefficients(pulse_shape, num_taps=31):\n", + " \"\"\"\n", + " Create a matched filter for a given pulse shape.\n", + " The matched filter maximizes SNR for known signals.\n", + " \"\"\"\n", + " if isinstance(pulse_shape, str):\n", + " if pulse_shape == 'gaussian':\n", + " # Gaussian pulse shape\n", + " t = np.linspace(-2, 2, num_taps)\n", + " coeffs = np.exp(-t**2 / 2)\n", + " elif pulse_shape == 'square':\n", + " coeffs = np.ones(num_taps)\n", + " else:\n", + " coeffs = np.random.randn(num_taps)\n", + " else:\n", + " coeffs = np.array(pulse_shape)\n", + " \n", + " # Normalize\n", + " coeffs = coeffs / np.sum(np.abs(coeffs))\n", + " coeffs = (coeffs / np.max(np.abs(coeffs)) * 32767).astype(int)\n", + " return coeffs\n", + " \n", + " # Example 2: Notch Filter for Interference Rejection\n", + " def notch_filter_coefficients(notch_freq_mhz, sample_rate_mhz=100, num_taps=31, Q=10):\n", + " \"\"\"\n", + " Design a notch filter to remove a specific interference frequency.\n", + " \"\"\"\n", + " nyquist = sample_rate_mhz / 2\n", + " normalized_notch = notch_freq_mhz / nyquist\n", + " \n", + " # Simple notch using a second-order IIR converted to FIR\n", + " b, a = signal.iirnotch(normalized_notch, Q)\n", + " \n", + " # Convert to FIR (truncate impulse response)\n", + " impulse = np.zeros(num_taps * 2)\n", + " impulse[0] = 1\n", + " response = signal.lfilter(b, a, impulse)\n", + " coeffs = response[:num_taps]\n", + " \n", + " coeffs = (coeffs / np.max(np.abs(coeffs)) * 32767).astype(int)\n", + " return coeffs\n", + " \n", + " # Example 3: Hilbert Transform for Envelope Detection\n", + " def hilbert_coefficients(num_taps=31):\n", + " \"\"\"\n", + " Hilbert transform coefficients for IQ demodulation.\n", + " \"\"\"\n", + " n = np.arange(num_taps) - (num_taps - 1) / 2\n", + " n[n == 0] = 1 # Avoid division by zero\n", + " coeffs = (1 - (-1)**n) / (np.pi * n)\n", + " coeffs = (coeffs / np.max(np.abs(coeffs)) * 32767).astype(int)\n", + " return coeffs\n", + " \n", + " print(\"Advanced DSP Techniques for QICK:\")\n", + " print(\"\\n1. Matched Filters\")\n", + " gaussian_mf = matched_filter_coefficients('gaussian')\n", + " print(f\" Gaussian matched filter taps: {gaussian_mf[:5]}...\")\n", + " \n", + " print(\"\\n2. Notch Filters for Interference Rejection\")\n", + " notch = notch_filter_coefficients(10, Q=30) # 10 MHz interference\n", + " print(f\" 10 MHz notch filter taps: {notch[:5]}...\")\n", + " \n", + " print(\"\\n3. Hilbert Transform for Envelope Detection\")\n", + " hilbert = hilbert_coefficients()\n", + " print(f\" Hilbert filter taps: {hilbert[:5]}...\")\n", + "\n", + "advanced_dsp_examples()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Summary\n", + "\n", + "You have learned:\n", + "- How to design and apply FIR filters for pulse shaping and signal processing\n", + "- How to dynamically tune DDS frequencies during sequences\n", + "- How to use hardware correlators for cross-channel measurements\n", + "- How to generate multi-tone signals for parallel operations\n", + "- Advanced DSP techniques like matched filtering and notch filtering\n", + "\n", + "**Key takeaways:**\n", + "- FIR filters enable real-time spectral shaping on DAC outputs\n", + "- Dynamic DDS tuning allows frequency modulation within sequences\n", + "- Hardware correlators provide real-time cross-correlation at high speed\n", + "- Multi-tone generation enables parallel control and readout\n", + "\n", + "**Next steps:** Proceed to [`13_Custom_Firmware_Integration.ipynb`](./13_Custom_Firmware_Integration.ipynb) to learn about adding custom Verilog modules to the QICK firmware." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/13_Custom_Firmware_Integration.ipynb b/docs/source/tutorials/13_Custom_Firmware_Integration.ipynb new file mode 100644 index 000000000..6eec0aad3 --- /dev/null +++ b/docs/source/tutorials/13_Custom_Firmware_Integration.ipynb @@ -0,0 +1,765 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 13 - Custom Firmware Integration\n", + "\n", + "**Objective:** Add custom Verilog/VHDL modules, integrate with AXI-lite interface, and rebuild the firmware. This notebook covers:\n", + "- Custom Verilog module development\n", + "- AXI-lite interface integration\n", + "- Firmware rebuild process\n", + "- Python interface to custom registers\n", + "- Debugging and verification\n", + "\n", + "## Prerequisites\n", + "- Vivado installation (2020.1 or later)\n", + "- QICK source code\n", + "- Basic Verilog/VHDL knowledge\n", + "- Familiarity with AXI bus protocol" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Setup" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Board model: RFSoC4x2\n", + "Software version: 0.2.405\n", + "Firmware timestamp: Mon Apr 6 16:32:19 2026\n", + "Available Generators (DACs): 2\n", + "Available Readouts (ADCs): 3\n", + "Reference Clock: 491.52 MHz\n", + "Number of tProc cores: 1\n" + ] + } + ], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import logging\n", + "import os\n", + "import subprocess\n", + "\n", + "from qick import QickSoc\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Connect to the board to check current firmware info\n", + "BITSTREAM_PATH = '/path/to/current_firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH)\n", + "\n", + "# Get firmware information\n", + "cfg = soc.get_cfg()\n", + "# Get firmware information\n", + "cfg = soc.get_cfg()\n", + "\n", + "print(f\"Board model: {cfg.get('board', 'Unknown')}\")\n", + "print(f\"Software version: {cfg.get('sw_version', 'Unknown')}\")\n", + "print(f\"Firmware timestamp: {cfg.get('fw_timestamp', 'Unknown')}\")\n", + "\n", + "# In this version, the channels are in 'gens' and 'readouts'.\n", + "print(f\"Available Generators (DACs): {len(cfg.get('gens', []))}\")\n", + "print(f\"Available Readouts (ADCs): {len(cfg.get('readouts', []))}\")\n", + "\n", + "# Useful Additional Information\n", + "if 'refclk_freq' in cfg:\n", + " print(f\"Reference Clock: {cfg['refclk_freq']} MHz\")\n", + "\n", + "# El tProc v2 suele estar en la lista de 'tprocs'\n", + "if 'tprocs' in cfg:\n", + " print(f\"Number of tProc cores: {len(cfg['tprocs'])}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Custom Verilog Module Example\n", + "\n", + "Here's a complete example of a custom Verilog module with AXI-lite interface. This module demonstrates:\n", + "- AXI-lite slave interface for register access\n", + "- Configurable parameters\n", + "- Simple DSP operation (multiply-accumulate)\n", + "- Status registers for debugging" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Custom Module Verilog Code\n", + "\n", + "Create a file named `custom_dsp_module.v`:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "verilog_code = '''\n", + "// Custom DSP Module for QICK\n", + "// Supports multiply-accumulate operations with AXI-lite interface\n", + "\n", + "module custom_dsp_module #(\n", + " parameter AXI_ADDR_WIDTH = 32,\n", + " parameter AXI_DATA_WIDTH = 32,\n", + " parameter NUM_REGS = 8\n", + ")(\n", + " input wire clk,\n", + " input wire rst,\n", + " \n", + " // AXI-lite slave interface\n", + " input wire [AXI_ADDR_WIDTH-1:0] s_axi_awaddr,\n", + " input wire s_axi_awvalid,\n", + " output wire s_axi_awready,\n", + " input wire [AXI_DATA_WIDTH-1:0] s_axi_wdata,\n", + " input wire s_axi_wvalid,\n", + " output wire s_axi_wready,\n", + " output wire [1:0] s_axi_bresp,\n", + " output wire s_axi_bvalid,\n", + " input wire s_axi_bready,\n", + " input wire [AXI_ADDR_WIDTH-1:0] s_axi_araddr,\n", + " input wire s_axi_arvalid,\n", + " output wire s_axi_arready,\n", + " output wire [AXI_DATA_WIDTH-1:0] s_axi_rdata,\n", + " output wire [1:0] s_axi_rresp,\n", + " output wire s_axi_rvalid,\n", + " input wire s_axi_rready,\n", + " \n", + " // Custom outputs (can connect to DACs or other modules)\n", + " output reg [31:0] custom_output,\n", + " output reg [31:0] mac_result,\n", + " \n", + " // Status outputs\n", + " output reg [31:0] status,\n", + " output reg interrupt\n", + ");\n", + "\n", + " // Register addresses\n", + " localparam REG_CONTROL = 8'h00;\n", + " localparam REG_STATUS = 8'h04;\n", + " localparam REG_VALUE_A = 8'h08;\n", + " localparam REG_VALUE_B = 8'h0C;\n", + " localparam REG_MAC_RESULT = 8'h10;\n", + " localparam REG_CUSTOM_OUT = 8'h14;\n", + " localparam REG_CONFIG = 8'h18;\n", + " localparam REG_VERSION = 8'hFC;\n", + " \n", + " // Internal registers\n", + " reg [AXI_DATA_WIDTH-1:0] regs [0:NUM_REGS-1];\n", + " \n", + " // Multiply-accumulate state\n", + " reg [63:0] accumulator;\n", + " reg [31:0] value_a, value_b;\n", + " reg mac_busy;\n", + " reg [7:0] mac_counter;\n", + " \n", + " // AXI state machine\n", + " reg [1:0] axi_state;\n", + " reg [AXI_ADDR_WIDTH-1:0] write_addr;\n", + " \n", + " // Initialize registers\n", + " integer i;\n", + " initial begin\n", + " for (i = 0; i < NUM_REGS; i = i + 1) begin\n", + " regs[i] = 32'd0;\n", + " end\n", + " regs[REG_VERSION >> 2] = 32'h00010001; // Version 1.1\n", + " accumulator = 64'd0;\n", + " mac_busy = 1'b0;\n", + " custom_output = 32'd0;\n", + " mac_result = 32'd0;\n", + " status = 32'd0;\n", + " interrupt = 1'b0;\n", + " end\n", + " \n", + " // AXI-lite write channel\n", + " assign s_axi_awready = (axi_state == 2'b01);\n", + " assign s_axi_wready = (axi_state == 2'b01);\n", + " assign s_axi_bresp = 2'b00;\n", + " assign s_axi_bvalid = (axi_state == 2'b10);\n", + " \n", + " // AXI-lite read channel\n", + " assign s_axi_arready = (axi_state == 2'b00);\n", + " assign s_axi_rdata = regs[s_axi_araddr[7:2]];\n", + " assign s_axi_rresp = 2'b00;\n", + " assign s_axi_rvalid = (axi_state == 2'b11);\n", + " \n", + " // AXI-lite state machine\n", + " always @(posedge clk) begin\n", + " if (rst) begin\n", + " axi_state <= 2'b00;\n", + " write_addr <= 32'd0;\n", + " end else begin\n", + " case (axi_state)\n", + " 2'b00: begin // Idle\n", + " if (s_axi_awvalid && s_axi_wvalid) begin\n", + " write_addr <= s_axi_awaddr;\n", + " axi_state <= 2'b01;\n", + " end else if (s_axi_arvalid) begin\n", + " axi_state <= 2'b11;\n", + " end\n", + " end\n", + " 2'b01: begin // Write data received\n", + " regs[write_addr[7:2]] <= s_axi_wdata;\n", + " axi_state <= 2'b10;\n", + " end\n", + " 2'b10: begin // Write response\n", + " if (s_axi_bready) begin\n", + " axi_state <= 2'b00;\n", + " end\n", + " end\n", + " 2'b11: begin // Read response\n", + " if (s_axi_rready) begin\n", + " axi_state <= 2'b00;\n", + " end\n", + " end\n", + " endcase\n", + " end\n", + " end\n", + " \n", + " // Multiply-Accumulate operation\n", + " always @(posedge clk) begin\n", + " if (rst) begin\n", + " accumulator <= 64'd0;\n", + " mac_busy <= 1'b0;\n", + " mac_counter <= 8'd0;\n", + " mac_result <= 32'd0;\n", + " end else begin\n", + " // Check for MAC start command\n", + " if (regs[REG_CONTROL >> 2][0] && !mac_busy) begin\n", + " value_a <= regs[REG_VALUE_A >> 2];\n", + " value_b <= regs[REG_VALUE_B >> 2];\n", + " accumulator <= 64'd0;\n", + " mac_counter <= regs[REG_CONFIG >> 2][7:0];\n", + " mac_busy <= 1'b1;\n", + " regs[REG_CONTROL >> 2][0] <= 1'b0; // Clear start bit\n", + " status[0] <= 1'b1; // MAC busy\n", + " end\n", + " \n", + " // Perform MAC iterations\n", + " if (mac_busy && (mac_counter > 0)) begin\n", + " accumulator <= accumulator + (value_a * value_b);\n", + " mac_counter <= mac_counter - 1;\n", + " end\n", + " \n", + " // MAC complete\n", + " if (mac_busy && (mac_counter == 0)) begin\n", + " mac_busy <= 1'b0;\n", + " mac_result <= accumulator[31:0];\n", + " regs[REG_MAC_RESULT >> 2] <= accumulator[31:0];\n", + " status[0] <= 1'b0; // MAC done\n", + " status[1] <= 1'b1; // MAC complete flag\n", + " interrupt <= 1'b1;\n", + " end\n", + " \n", + " // Clear interrupt on read\n", + " if (s_axi_arvalid && (s_axi_araddr[7:2] == (REG_STATUS >> 2))) begin\n", + " interrupt <= 1'b0;\n", + " status[1] <= 1'b0;\n", + " end\n", + " end\n", + " end\n", + " \n", + " // Custom output from register\n", + " always @(posedge clk) begin\n", + " if (rst) begin\n", + " custom_output <= 32'd0;\n", + " end else begin\n", + " custom_output <= regs[REG_CUSTOM_OUT >> 2];\n", + " end\n", + " end\n", + " \n", + "endmodule\n", + "'''\n", + "\n", + "print(\"=== Custom Verilog Module ===\")\n", + "print(verilog_code[:500] + \"...\")\n", + "print(f\"\\nTotal module size: {len(verilog_code)} characters\")\n", + "print(\"\\nKey features:\")\n", + "print(\" - AXI-lite slave interface for register access\")\n", + "print(\" - Multiply-accumulate (MAC) operation\")\n", + "print(\" - Status and interrupt generation\")\n", + "print(\" - Configurable parameters\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Integration Steps\n", + "\n", + "Follow these steps to integrate the custom module into the QICK firmware." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_integration_steps():\n", + " \"\"\"Print the firmware integration procedure.\"\"\"\n", + " \n", + " steps = \"\"\"\n", + " ========================================\n", + " STEP 1: Add Module to QICK Source\n", + " ========================================\n", + " \n", + " # Copy the Verilog file to the QICK IP directory\n", + " cp custom_dsp_module.v $QICK_PATH/firmware/ip/\n", + " \n", + " # Add the module to the IP catalog (create a simple wrapper if needed)\n", + " cd $QICK_PATH/firmware\n", + " \n", + " ========================================\n", + " STEP 2: Update Block Design (TCL Script)\n", + " ========================================\n", + " \n", + " # Create a TCL script to add the module to the block design\n", + " cat > add_custom_module.tcl << 'EOF'\n", + " # Create and configure custom module cell\n", + " create_bd_cell -type ip -vlnv user:user:custom_dsp_module:1.0 custom_dsp_module_0\n", + " \n", + " # Connect AXI-lite interface to interconnect\n", + " connect_bd_intf_net -intf_net axi_interconnect_0_m00_axi \\\\\n", + " [get_bd_intf_pins custom_dsp_module_0/s_axi]\n", + " \n", + " # Connect clocks and resets\n", + " connect_bd_net -net clk_wiz_0_clk_out1 \\\\\n", + " [get_bd_pins custom_dsp_module_0/clk]\n", + " connect_bd_net -net rst_clk_wiz_0_100M_peripheral_aresetn \\\\\n", + " [get_bd_pins custom_dsp_module_0/rst]\n", + " \n", + " # Assign address range (check available address space)\n", + " assign_bd_address -target_address_space /processing_system7_0/Data \\\\\n", + " [get_bd_addr_segs custom_dsp_module_0/s_axi/reg0] \\\\\n", + " -range 0x1000 -offset 0x43C00000\n", + " EOF\n", + " \n", + " # Run TCL script in Vivado\n", + " vivado -source add_custom_module.tcl $QICK_PATH/firmware/projects/qick_tprocv2_4x2_standard_b_5/qick_tprocv2_4x2_standard_b_5.xpr\n", + " \n", + " ========================================\n", + " STEP 3: Rebuild Firmware\n", + " ========================================\n", + " \n", + " cd $QICK_PATH/firmware\n", + " make clean\n", + " make all\n", + " \n", + " # The new bitstream will be in:\n", + " # projects/qick_tprocv2_4x2_standard_b_5/out/qick_4x2.bit\n", + " \"\"\"\n", + " \n", + " print(steps)\n", + "\n", + "print_integration_steps()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Python Interface to Custom Module\n", + "\n", + "Once the firmware is rebuilt with your custom module, you can interface with it from Python using AXI-lite reads and writes." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class CustomDSPModule:\n", + " \"\"\"Python interface for the custom DSP module.\"\"\"\n", + " \n", + " # Register offsets (must match Verilog defines)\n", + " REG_CONTROL = 0x00\n", + " REG_STATUS = 0x04\n", + " REG_VALUE_A = 0x08\n", + " REG_VALUE_B = 0x0C\n", + " REG_MAC_RESULT = 0x10\n", + " REG_CUSTOM_OUT = 0x14\n", + " REG_CONFIG = 0x18\n", + " REG_VERSION = 0xFC\n", + " \n", + " def __init__(self, soc, base_addr=0x43C00000):\n", + " \"\"\"\n", + " Initialize the custom module interface.\n", + " \n", + " Parameters:\n", + " - soc: QickSoc instance\n", + " - base_addr: AXI base address of the custom module\n", + " \"\"\"\n", + " self.soc = soc\n", + " self.base = base_addr\n", + " self._check_version()\n", + " \n", + " def _check_version(self):\n", + " \"\"\"Verify the module is present and get version.\"\"\"\n", + " try:\n", + " version = self.read_reg(self.REG_VERSION)\n", + " major = (version >> 16) & 0xFFFF\n", + " minor = version & 0xFFFF\n", + " print(f\"Custom module detected: version {major}.{minor}\")\n", + " except Exception as e:\n", + " print(f\"Warning: Could not read custom module version: {e}\")\n", + " \n", + " def write_reg(self, offset, value):\n", + " \"\"\"Write to a register.\"\"\"\n", + " self.soc.axilite_write(self.base + offset, value)\n", + " \n", + " def read_reg(self, offset):\n", + " \"\"\"Read from a register.\"\"\"\n", + " return self.soc.axilite_read(self.base + offset)\n", + " \n", + " def set_value_a(self, value):\n", + " \"\"\"Set value A for MAC operation.\"\"\"\n", + " self.write_reg(self.REG_VALUE_A, value & 0xFFFFFFFF)\n", + " \n", + " def set_value_b(self, value):\n", + " \"\"\"Set value B for MAC operation.\"\"\"\n", + " self.write_reg(self.REG_VALUE_B, value & 0xFFFFFFFF)\n", + " \n", + " def set_custom_output(self, value):\n", + " \"\"\"Set the custom output value.\"\"\"\n", + " self.write_reg(self.REG_CUSTOM_OUT, value & 0xFFFFFFFF)\n", + " \n", + " def set_config(self, num_iterations):\n", + " \"\"\"Configure MAC operation parameters.\"\"\"\n", + " self.write_reg(self.REG_CONFIG, num_iterations & 0xFF)\n", + " \n", + " def start_mac(self):\n", + " \"\"\"Start the multiply-accumulate operation.\"\"\"\n", + " self.write_reg(self.REG_CONTROL, 0x01)\n", + " \n", + " def is_mac_busy(self):\n", + " \"\"\"Check if MAC operation is in progress.\"\"\"\n", + " status = self.read_reg(self.REG_STATUS)\n", + " return (status & 0x01) != 0\n", + " \n", + " def get_mac_result(self):\n", + " \"\"\"Get the MAC result (blocking until complete).\"\"\"\n", + " # Wait for completion (or check interrupt)\n", + " import time\n", + " timeout = 0.001 # 1 ms timeout\n", + " start_time = time.time()\n", + " \n", + " while self.is_mac_busy():\n", + " if time.time() - start_time > timeout:\n", + " raise TimeoutError(\"MAC operation timed out\")\n", + " \n", + " return self.read_reg(self.REG_MAC_RESULT)\n", + " \n", + " def multiply_accumulate(self, a, b, num_iterations=1):\n", + " \"\"\"\n", + " Perform a multiply-accumulate operation on hardware.\n", + " \n", + " Returns a * b * num_iterations (in hardware, with 32-bit precision)\n", + " \"\"\"\n", + " self.set_value_a(a)\n", + " self.set_value_b(b)\n", + " self.set_config(num_iterations)\n", + " self.start_mac()\n", + " return self.get_mac_result()\n", + " \n", + " def get_status_string(self):\n", + " \"\"\"Get human-readable status.\"\"\"\n", + " status = self.read_reg(self.REG_STATUS)\n", + " bits = []\n", + " if status & 0x01:\n", + " bits.append(\"MAC_BUSY\")\n", + " if status & 0x02:\n", + " bits.append(\"MAC_DONE\")\n", + " return f\"Status: {', '.join(bits) if bits else 'IDLE'} (0x{status:08X})\"\n", + "\n", + "\n", + "# Example usage (after firmware is rebuilt)\n", + "def example_custom_module_usage():\n", + " \"\"\"Example of using the custom module.\"\"\"\n", + " \n", + " print(\"=== Custom Module Usage Example ===\\n\")\n", + " \n", + " # Initialize the custom module (use correct base address)\n", + " # custom = CustomDSPModule(soc, base_addr=0x43C00000)\n", + " \n", + " # Test basic operations\n", + " # custom.set_custom_output(0xDEADBEEF)\n", + " # print(f\"Custom output set to: 0x{custom.read_reg(CustomDSPModule.REG_CUSTOM_OUT):08X}\")\n", + " \n", + " # Perform MAC operation\n", + " # result = custom.multiply_accumulate(1000, 2000, num_iterations=5)\n", + " # print(f\"MAC result: {result}\")\n", + " # Expected: 1000 * 2000 * 5 = 10,000,000\n", + " \n", + " print(\"Example operations:\")\n", + " print(\" - Set custom output: custom.set_custom_output(0xDEADBEEF)\")\n", + " print(\" - Read status: custom.get_status_string()\")\n", + " print(\" - MAC operation: result = custom.multiply_accumulate(1000, 2000, 5)\")\n", + "\n", + "example_custom_module_usage()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Debugging Custom Firmware\n", + "\n", + "Here are techniques for debugging custom firmware modules." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def debug_firmware_techniques():\n", + " \"\"\"Debugging techniques for custom firmware.\"\"\"\n", + " \n", + " print(\"=== Firmware Debugging Techniques ===\\n\")\n", + " \n", + " print(\"1. Integrated Logic Analyzer (ILA)\")\n", + " print(\" - Add ILA cores to capture internal signals\")\n", + " print(\" - Use Vivado hardware manager to view waveforms\")\n", + " print(\" - Example TCL:\")\n", + " print(\" create_bd_cell -type ip -vlnv xilinx.com:ip:ila:6.2 ila_0\")\n", + " print(\" connect_bd_net [get_bd_pins custom_module/signal] [get_bd_pins ila_0/probe0]\\n\")\n", + " \n", + " print(\"2. Virtual I/O (VIO) for Real-Time Control\")\n", + " print(\" - Use VIO to read/write signals during runtime\")\n", + " print(\" - Example TCL:\")\n", + " print(\" create_bd_cell -type ip -vlnv xilinx.com:ip:vio:3.0 vio_0\\n\")\n", + " \n", + " print(\"3. AXI Verification\")\n", + " print(\" - Use AXI VIP for protocol checking\")\n", + " print(\" - Simulate with XSIM or ModelSim\")\n", + " print(\" - Check address mapping in the address editor\\n\")\n", + " \n", + " print(\"4. Python-Based Debugging\")\n", + " print(\" - Read/write registers to verify functionality\")\n", + " print(\" - Use timeout-based polling for status bits\")\n", + " print(\" - Implement diagnostic functions\")\n", + " print(\"\")\n", + " \n", + " print(\"5. Common Issues and Solutions\")\n", + " issues = [\n", + " (\"Clock domain crossing\", \"Use proper synchronizers or CDC modules\"),\n", + " (\"AXI address conflict\", \"Check address ranges in address editor\"),\n", + " (\"Timing closure failures\", \"Add pipeline registers or reduce logic depth\"),\n", + " (\"Missing reset connections\", \"Verify all modules receive reset signals\"),\n", + " (\"Incorrect AXI handshake\", \"Follow AXI-lite protocol specification\")\n", + " ]\n", + " \n", + " for issue, solution in issues:\n", + " print(f\" - {issue}: {solution}\")\n", + "\n", + "debug_firmware_techniques()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Firmware Verification Test\n", + "\n", + "After rebuilding the firmware, run this verification test to ensure everything works." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def verify_custom_firmware(soc, custom_module, expected_version=\"1.0\"):\n", + " \"\"\"\n", + " Comprehensive verification test for custom firmware.\n", + " \n", + " Parameters:\n", + " - soc: QickSoc instance\n", + " - custom_module: CustomDSPModule instance\n", + " - expected_version: Expected version string\n", + " \n", + " Returns:\n", + " - True if all tests pass, False otherwise\n", + " \"\"\"\n", + " print(\"=== Firmware Verification ===\\n\")\n", + " \n", + " tests_passed = 0\n", + " tests_total = 0\n", + " \n", + " # Test 1: Version check\n", + " tests_total += 1\n", + " try:\n", + " version = custom_module.read_reg(CustomDSPModule.REG_VERSION)\n", + " version_str = f\"{(version >> 16) & 0xFFFF}.{version & 0xFFFF}\"\n", + " print(f\"✓ Version: {version_str}\")\n", + " tests_passed += 1\n", + " except Exception as e:\n", + " print(f\"✗ Version check failed: {e}\")\n", + " \n", + " # Test 2: Register read/write\n", + " tests_total += 1\n", + " test_value = 0x12345678\n", + " try:\n", + " custom_module.write_reg(CustomDSPModule.REG_CUSTOM_OUT, test_value)\n", + " read_value = custom_module.read_reg(CustomDSPModule.REG_CUSTOM_OUT)\n", + " if read_value == test_value:\n", + " print(f\"✓ Register write/read: OK (0x{read_value:08X})\")\n", + " tests_passed += 1\n", + " else:\n", + " print(f\"✗ Register mismatch: wrote 0x{test_value:08X}, read 0x{read_value:08X}\")\n", + " except Exception as e:\n", + " print(f\"✗ Register test failed: {e}\")\n", + " \n", + " # Test 3: MAC operation\n", + " tests_total += 1\n", + " try:\n", + " a, b, n = 100, 200, 3\n", + " expected = a * b * n\n", + " result = custom_module.multiply_accumulate(a, b, n)\n", + " if result == expected:\n", + " print(f\"✓ MAC test: {a} * {b} * {n} = {result}\")\n", + " tests_passed += 1\n", + " else:\n", + " print(f\"✗ MAC test: expected {expected}, got {result}\")\n", + " except Exception as e:\n", + " print(f\"✗ MAC test failed: {e}\")\n", + " \n", + " # Test 4: Basic QICK functionality\n", + " tests_total += 1\n", + " try:\n", + " cfg = soc.get_cfg()\n", + " print(f\"✓ QICK readout: {cfg['adcs']} ADCs, {cfg['dacs']} DACs\")\n", + " tests_passed += 1\n", + " except Exception as e:\n", + " print(f\"✗ QICK test failed: {e}\")\n", + " \n", + " print(f\"\\nTest Results: {tests_passed}/{tests_total} passed\")\n", + " return tests_passed == tests_total\n", + "\n", + "print(\"Verification function ready.\")\n", + "print(\"After rebuilding firmware, run:\")\n", + "print(\" custom = CustomDSPModule(soc, base_addr=0x43C00000)\")\n", + "print(\" verify_custom_firmware(soc, custom)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Resources and References" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def print_resources():\n", + " \"\"\"Print useful resources for custom firmware development.\"\"\"\n", + " \n", + " resources = {\n", + " \"QICK Documentation\": \"https://qick.readthedocs.io/en/latest/firmware.html\",\n", + " \"Vivado AXI Reference\": \"https://www.xilinx.com/support/documentation/ip_documentation/axi_ref_guide/latest/ug1037-vivado-axi-reference-guide.pdf\",\n", + " \"QICK GitHub\": \"https://github.com/openquantumhardware/qick\",\n", + " \"Xilinx Vivado Documentation\": \"https://www.xilinx.com/support/documentation-navigation/design-hubs/dh004-ultrascale-plus-vivado-design-hub.html\",\n", + " \"AXI-lite Protocol\": \"https://developer.arm.com/documentation/ihi0022/latest/\"\n", + " }\n", + " \n", + " print(\"=== Useful Resources ===\\n\")\n", + " for name, url in resources.items():\n", + " print(f\"- {name}: {url}\")\n", + " \n", + " print(\"\\n=== Key Commands Reference ===\\n\")\n", + " cmds = [\n", + " (\"make all\", \"Build firmware\"),\n", + " (\"make clean\", \"Clean build artifacts\"),\n", + " (\"vivado -source script.tcl project.xpr\", \"Run TCL script in Vivado\"),\n", + " (\"report_ip_status\", \"Check IP status in Vivado\"),\n", + " (\"validate_bd_design\", \"Validate block design\"),\n", + " (\"write_bitstream\", \"Generate bitstream\"),\n", + " ]\n", + " \n", + " for cmd, desc in cmds:\n", + " print(f\" {cmd}: {desc}\")\n", + "\n", + "print_resources()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Summary\n", + "\n", + "You have learned:\n", + "- How to create custom Verilog modules with AXI-lite interfaces\n", + "- Integration steps for adding custom modules to QICK firmware\n", + "- How to rebuild the firmware with custom modules\n", + "- Python interface development for custom registers\n", + "- Debugging techniques for custom firmware\n", + "- Verification procedures for new firmware\n", + "\n", + "**Key takeaways:**\n", + "- Custom modules extend QICK functionality for specialized applications\n", + "- AXI-lite provides a standard interface for register access\n", + "- Use Vivado's IP integrator for block design modifications\n", + "- Always verify custom firmware with test routines\n", + "- Use ILA/VIO cores for hardware debugging\n", + "\n", + "**Next steps:**\n", + "\n", + "- For multi-board synchronization without XCOM hardware, see [`10_Multi_Board_Synchronization.ipynb`](./10_Multi_Board_Synchronization.ipynb)\n", + "- For advanced multi-board networking with XCOM, see [`14_XCOM_Network_Synchronization.ipynb`](./14_XCOM_Network_Synchronization.ipynb)\n", + "- Review the QICK firmware documentation for detailed build instructions\n", + "- Experiment with simple custom modules before complex designs\n", + "- Join the QICK community for support and collaboration" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.4" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/14_XCOM_Network_Synchronization.ipynb b/docs/source/tutorials/14_XCOM_Network_Synchronization.ipynb new file mode 100644 index 000000000..87aa82c6a --- /dev/null +++ b/docs/source/tutorials/14_XCOM_Network_Synchronization.ipynb @@ -0,0 +1,1067 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 14 - XCOM Network Synchronization\n", + "\n", + "**Objective:** Learn how to synchronize multiple QICK boards and exchange low-latency data using the XCOM network. This notebook covers:\n", + "\n", + "- XCOM architecture overview (full mesh network, deterministic latency)\n", + "- Hardware setup (FMC transceiver board, external hub)\n", + "- Initializing the XCOM network (AUTO_ID, master/slave configuration)\n", + "- Sending and receiving data between boards (8-bit, 16-bit, 32-bit)\n", + "- Loopback testing and latency measurement\n", + "- tProc integration for real-time communication\n", + "- Debugging XCOM (status registers, debug registers)\n", + "- Synchronizing tProc absolute clocks across multiple boards\n", + "\n", + "**Prerequisites:**\n", + "- Two or more QICK boards with XCOM FMC transceiver boards\n", + "- External XCOM hub connecting all boards\n", + "- Stable external reference clock (e.g., rubidium clock)\n", + "- Completed basic tutorials (00-05)\n", + "\n", + "**References:**\n", + "- XCOM Paper: arXiv:2603.18977v1\n", + "- QICK XCOM driver: `qick.ip.QICK_Xcom`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. XCOM Overview\n", + "\n", + "XCOM (X - Communication) is a **full mesh network** that enables:\n", + "\n", + "- **Synchronization** of multiple QICK boards with <100 ps skew\n", + "- **Low-latency communication** (186 ns per 32-bit word, can be reduced to 62 ns)\n", + "- **Deterministic message exchange** without central control\n", + "- **All-to-all simultaneous communication** on different channels\n", + "- **tProc integration** for real-time data exchange\n", + "\n", + "### Hardware Requirements\n", + "\n", + "```\n", + "┌─────────────────────────────────────────────────────────────────────────────┐\n", + "│ XCOM Network Topology │\n", + "├─────────────────────────────────────────────────────────────────────────────┤\n", + "│ │\n", + "│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │\n", + "│ │ QICK #1 │ │ QICK #2 │ │ QICK #3 │ │ QICK #4 │ │\n", + "│ │ (Master) │ │ (Slave) │ │ (Slave) │ │ (Slave) │ │\n", + "│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │\n", + "│ │ │ │ │ │\n", + "│ ▼ ▼ ▼ ▼ │\n", + "│ ┌─────────────────────────────────────────────────────────────────┐ │\n", + "│ │ XCOM Hub (Fanout Buffers) │ │\n", + "│ └─────────────────────────────────────────────────────────────────┘ │\n", + "│ │\n", + "│ • Each board has a dedicated Tx channel (LVDS @ up to 312.9 MHz) │\n", + "│ • All boards listen on all Rx channels (full mesh) │\n", + "│ • External reference clock (e.g., 100 MHz rubidium) for frequency sync │\n", + "│ │\n", + "└─────────────────────────────────────────────────────────────────────────────┘\n", + "```\n", + "\n", + "**Supported boards:** Up to 15 boards (prototype supports 5)\n", + "\n", + "**Key capabilities:**\n", + "- Sub-picosecond timing jitter with nested zero-delay PLL mode\n", + "- Multi-tile sync (MTS) for DAC/ADC alignment across boards\n", + "- 48-bit absolute clock counter synchronized across all boards\n", + "- Deterministic communication latency (independent of network traffic)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Setup and Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import time\n", + "import logging\n", + "\n", + "from qick import *\n", + "from qick.asm_v2 import AveragerProgramV2\n", + "from qick.tprocv2_assembler import Assembler\n", + "\n", + "# Setup logging\n", + "logging.basicConfig(level=logging.INFO, format='%(levelname)-8s [%(filename)s:%(lineno)d] %(message)s')\n", + "\n", + "# Connect to the board (adjust path to your firmware with XCOM support)\n", + "# For multi-board setup, create separate Soc instances for each board\n", + "BITSTREAM_PATH = '/path/to/your/xcom_firmware.bit' # <--- CHANGE THIS\n", + "soc = QickSoc(BITSTREAM_PATH, external_clk=True, force_init_clks=True)\n", + "soccfg = soc\n", + "\n", + "print(f\"Connected to QICK board\")\n", + "print(f\"Firmware version: {soccfg.get('fw_version', 'Unknown')}\")\n", + "print(f\"XCOM available: {hasattr(soc, 'xcom_0')}\")\n", + "\n", + "# For multi-board experiments, you would connect to each board separately:\n", + "# soc_master = QickSoc('master_firmware.bit', external_clk=True)\n", + "# soc_slave1 = QickSoc('slave1_firmware.bit', external_clk=True)\n", + "# soc_slave2 = QickSoc('slave2_firmware.bit', external_clk=True)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Initialize Clock and Tile Synchronization\n", + "\n", + "Before using XCOM, the board clocks and DAC/ADC tiles must be synchronized." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Initialize tile synchronization (required for multi-board alignment)\n", + "print(\"Initializing tile synchronization...\")\n", + "soc.init_tile_sync()\n", + "soc.verify_clock_tree()\n", + "soc.sync_tiles()\n", + "print(\"Tile synchronization complete.\")\n", + "\n", + "# Verify external clock is locked\n", + "print(f\"External clock locked: {soc.check_ext_clk()}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. XCOM Driver Overview\n", + "\n", + "The `QICK_Xcom` class provides the Python interface to the XCOM hardware. It inherits from `SocIP` and uses AXI-lite for register access." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Display XCOM registers and opcodes\n", + "print(\"=== XCOM Registers ===\")\n", + "for reg_name, reg_addr in soc.xcom_0.REGISTERS.items():\n", + " print(f\" {reg_name:15} : address {reg_addr}\")\n", + "\n", + "print(\"\\n=== XCOM Opcodes ===\")\n", + "for op_name, op_code in soc.xcom_0.opcodes.items():\n", + " print(f\" {op_name:20} : {op_code}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 3.1 Key XCOM Operations\n", + "\n", + "| Method | Opcode | Description |\n", + "|--------|--------|-------------|\n", + "| `auto_id()` | XCOM_AUTO_ID | Auto-assign network IDs to all boards |\n", + "| `set_local_id(chid)` | XCOM_SET_ID | Manually set this board's ID |\n", + "| `send_byte(data, dst, reg)` | XCOM_SEND_8BIT_1/2 | Send 8 bits to board `dst` |\n", + "| `send_half_word(data, dst, reg)` | XCOM_SEND_16BIT_1/2 | Send 16 bits to board `dst` |\n", + "| `send_word(data, dst, reg)` | XCOM_SEND_32BIT_1/2 | Send 32 bits to board `dst` |\n", + "| `set_flag(dst)` | XCOM_SET_FLAG | Set flag on destination board |\n", + "| `clear_flag(dst)` | XCOM_CLEAR_FLAG | Clear flag on destination board |\n", + "| `run_cmd(cmd, dt1, dt2)` | (any) | Execute any opcode with data |\n", + "\n", + "**Note:** `reg` parameter (1 or 2) selects which destination register to write to." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Network Configuration\n", + "\n", + "### 4.1 Reset and Auto-ID\n", + "\n", + "The first step is to reset the XCOM network and assign IDs to all connected boards." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Reset the XCOM network\n", + "print(\"Resetting XCOM network...\")\n", + "soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_RST'], 0, 0)\n", + "time.sleep(0.1)\n", + "\n", + "# Run AUTO_ID to discover and assign IDs to all boards\n", + "print(\"Running AUTO_ID...\")\n", + "soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_AUTO_ID'], 0, 0)\n", + "time.sleep(0.5)\n", + "\n", + "# Set local board ID (master board in this example)\n", + "BOARD_ID_MASTER = 2\n", + "soc.xcom_0.set_local_id(BOARD_ID_MASTER)\n", + "print(f\"Local board ID set to: {BOARD_ID_MASTER}\")\n", + "\n", + "# Re-run AUTO_ID to confirm\n", + "soc.xcom_0.auto_id()\n", + "time.sleep(0.1)\n", + "\n", + "# Configure XCOM (enable certain features)\n", + "# 0x400 typically enables the XCOM interface\n", + "soc.xcom_0.xcom_cfg = 0 + 0x400\n", + "print(f\"XCOM config: 0x{soc.xcom_0.xcom_cfg:04X}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 4.2 Verify Configuration\n", + "\n", + "Check the status and register values to ensure proper configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Print XCOM status\n", + "soc.xcom_0.print_status()\n", + "\n", + "# Print all AXI registers\n", + "print(\"\\n\")\n", + "soc.xcom_0.print_axi_regs()\n", + "\n", + "# Print debug information\n", + "print(\"\\n\")\n", + "soc.xcom_0.print_debug()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Sending Data Between Boards\n", + "\n", + "### 5.1 Sending 8-bit Data\n", + "\n", + "Use `send_byte()` to transmit 8-bit values to another board." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Send 8-bit data to another board (e.g., board ID 5)\n", + "DEST_BOARD = 5\n", + "test_byte = 0xAA\n", + "\n", + "print(f\"Sending 8-bit data (0x{test_byte:02X}) to board {DEST_BOARD}...\")\n", + "soc.xcom_0.send_byte(test_byte, DEST_BOARD, reg=2)\n", + "print(\"Data sent.\")\n", + "\n", + "# For multi-board setup, you would read the data on the destination board:\n", + "# received_data = soc_slave.xcom_0.mem # Read from XCOM memory" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.2 Sending 16-bit Data\n", + "\n", + "Use `send_half_word()` to transmit 16-bit values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Send 16-bit data\n", + "test_half_word = 0xABCD\n", + "\n", + "print(f\"Sending 16-bit data (0x{test_half_word:04X}) to board {DEST_BOARD}...\")\n", + "soc.xcom_0.send_half_word(test_half_word, DEST_BOARD, reg=2)\n", + "print(\"Data sent.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.3 Sending 32-bit Data\n", + "\n", + "Use `send_word()` to transmit 32-bit values." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Send 32-bit data\n", + "test_word = 0xDEADBEEF\n", + "\n", + "print(f\"Sending 32-bit data (0x{test_word:08X}) to board {DEST_BOARD}...\")\n", + "soc.xcom_0.send_word(test_word, DEST_BOARD, reg=2)\n", + "print(\"Data sent.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 5.4 Writing to XCOM Memory\n", + "\n", + "XCOM has internal memory (16x32-bit) that can be written to and read from." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Write to XCOM memory on destination board\n", + "dest_addr = 5\n", + "data_to_write = 350\n", + "\n", + "print(f\"Writing {data_to_write} to memory address {dest_addr} on board {DEST_BOARD}\")\n", + "soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_WRITE_MEM'], dest_addr, data_to_write)\n", + "\n", + "# Read back the memory\n", + "print(\"\\nReading XCOM memory (local board):\")\n", + "print(\"addr\\tvalue\")\n", + "for i in range(16):\n", + " soc.xcom_0.axi_addr = i\n", + " print(f\"{i:3d}\\t{soc.xcom_0.mem}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. tProc Integration for XCOM\n", + "\n", + "XCOM can be controlled directly from the tProc using assembly instructions. The `PA` (Peripheral Access) instruction is the primary mechanism for tProc-XCOM interaction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example assembly program for XCOM communication from tProc\n", + "xcom_tproc_asm = \"\"\"\n", + "// XCOM Communication Test Program (tProc side)\n", + "// Sends data to another board via XCOM\n", + "\n", + "// Initialize registers\n", + "REG_WR r1 imm #1 // Configuration register 1\n", + "REG_WR r2 imm #2 // Configuration register 2\n", + "REG_WR r3 imm #3 // Configuration register 3\n", + "REG_WR r4 imm #4 // Configuration register 4\n", + "REG_WR r5 imm #5 // Destination board ID\n", + "REG_WR r6 imm #424 // Data to send\n", + "\n", + "// Write to XCOM control/configuration\n", + "// ctrl_csf_qpa = clear flag in cfg/cntl\n", + "// cfg_src_qpa = configure source\n", + "\n", + "// Trigger output pin for scope synchronization\n", + "TRIG p0 set\n", + "\n", + "// Write to XCOM memory (WRITE_MEM operation)\n", + "REG_WR s_ctrl imm ctrl_csf_qpa // Clear flag in cfg/cntl\n", + "WAIT qpa_rdy // Wait for peripheral ready\n", + "PA 7 r2 r6 // Peripheral access: write r6 using config in r2\n", + "WAIT qpa_dt // Wait for data transfer complete\n", + "\n", + "// Clear trigger\n", + "TRIG p0 clr\n", + "\n", + "// Set flag on destination board\n", + "WAIT qpa_rdy\n", + "PA 1 r5 // Set flag on board in r5\n", + "WAIT qpa_dt\n", + "\n", + ".END\n", + "\"\"\"\n", + "\n", + "# Compile the assembly\n", + "p_txt, p_bin = Assembler.str_asm2bin(xcom_tproc_asm)\n", + "print(f\"Assembly compiled: {len(p_bin)} instructions\")\n", + "print(\"\\nAssembly code:\")\n", + "print(p_txt)\n", + "\n", + "# Load the program into tProc program memory\n", + "# Note: The method name may vary based on your firmware version\n", + "# soc.qick_processor_0.load_mem(mem_sel='pmem', buff_in=p_bin, check=False)\n", + "print(\"\\nProgram ready to load to tProc.\")\n", + "print(\"Uncomment the load_mem line and adjust the method name as needed for your firmware.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 6.1 tProc Peripheral Access (PA) Instruction\n", + "\n", + "The `PA` instruction is used for peripheral access. The syntax is:\n", + "\n", + "```\n", + "PA \n", + "```\n", + "\n", + "Where:\n", + "- `cfg_reg`: Configuration register (controls the operation)\n", + "- `addr_reg`: Address/target register (e.g., destination board ID)\n", + "- `data_reg`: Data register (value to send)\n", + "\n", + "**Common PA operations:**\n", + "\n", + "| PA cfg | Operation | Description |\n", + "|--------|-----------|-------------|\n", + "| 0 | CLEAR_FLAG | Clear flag on destination board |\n", + "| 1 | SET_FLAG | Set flag on destination board |\n", + "| 6 | XCOM QCTRL | XCOM control operation |\n", + "| 7 | WRITE_MEM | Write to XCOM memory |" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Master-Slave Communication Example\n", + "\n", + "In a multi-board setup, one board acts as the **master** and others as **slaves**. The master can:\n", + "- Broadcast reset/start commands to synchronize absolute clocks\n", + "- Send data to specific slaves\n", + "- Receive data from slaves\n", + "- Coordinate multi-board experiments\n", + "\n", + "### 7.1 Master Board Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Master board setup (run on the board designated as master)\n", + "def setup_master_board(soc, master_id=2):\n", + " \"\"\"Configure a board as XCOM master.\"\"\"\n", + " print(\"Setting up master board...\")\n", + " \n", + " # Reset XCOM\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_RST'], 0, 0)\n", + " time.sleep(0.1)\n", + " \n", + " # Auto-assign IDs\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_AUTO_ID'], 0, 0)\n", + " time.sleep(0.5)\n", + " \n", + " # Set local ID\n", + " soc.xcom_0.set_local_id(master_id)\n", + " \n", + " # Configure XCOM\n", + " soc.xcom_0.xcom_cfg = 0 + 0x400 + 0x100 # Additional config for master\n", + " \n", + " print(f\"Master board configured with ID: {master_id}\")\n", + " return True\n", + "\n", + "# Slave board configuration (run on each slave board)\n", + "def setup_slave_board(soc, slave_id):\n", + " \"\"\"Configure a board as XCOM slave.\"\"\"\n", + " print(f\"Setting up slave board with ID: {slave_id}...\")\n", + " \n", + " # Reset XCOM\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_RST'], 0, 0)\n", + " time.sleep(0.1)\n", + " \n", + " # Auto-assign IDs\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_AUTO_ID'], 0, 0)\n", + " time.sleep(0.5)\n", + " \n", + " # Set local ID\n", + " soc.xcom_0.set_local_id(slave_id)\n", + " \n", + " # Configure XCOM\n", + " soc.xcom_0.xcom_cfg = 0 + 0x400\n", + " \n", + " print(f\"Slave board configured with ID: {slave_id}\")\n", + " return True\n", + "\n", + "print(\"Master/slave configuration functions defined.\")\n", + "print(\"\\nFor multi-board experiments:\")\n", + "print(\" master = setup_master_board(soc_master, master_id=2)\")\n", + "print(\" slave1 = setup_slave_board(soc_slave1, slave_id=5)\")\n", + "print(\" slave2 = setup_slave_board(soc_slave2, slave_id=7)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 7.2 Sending Data from Master to Slave\n", + "\n", + "Once configured, the master can send data to any slave." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Master sending a ramp pattern to a slave\n", + "def master_send_ramp(soc_master, dest_slave_id, num_values=100):\n", + " \"\"\"Send a ramp of values from master to slave.\"\"\"\n", + " print(f\"Master sending {num_values} values to slave {dest_slave_id}...\")\n", + " \n", + " start_time = time.time()\n", + " \n", + " for i in range(num_values):\n", + " # Send 16-bit value\n", + " soc_master.xcom_0.send_half_word(i, dest_slave_id, reg=2)\n", + " # Optional: wait for acknowledgment\n", + " # while soc_master.xcom_0.mem == 0:\n", + " # pass\n", + " \n", + " elapsed = time.time() - start_time\n", + " print(f\"Sent {num_values} values in {elapsed:.3f}s ({num_values/elapsed:.0f} values/sec)\")\n", + " return elapsed\n", + "\n", + "# On the slave side, you would read incoming data:\n", + "# def slave_receive_data(soc_slave, num_values=100):\n", + "# data = []\n", + "# for i in range(num_values):\n", + "# data.append(soc_slave.xcom_0.mem)\n", + "# return data\n", + "\n", + "print(\"Master-to-slave communication functions defined.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Loopback Testing\n", + "\n", + "Loopback testing validates XCOM functionality by sending data to the same board (self-communication). This is useful for debugging without multiple boards." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Loopback test: write to self\n", + "def xcom_loopback_test(soc, num_transfers=1000):\n", + " \"\"\"\n", + " Perform XCOM loopback test by writing to self.\n", + " Returns list of received data and measured latency.\n", + " \"\"\"\n", + " dest = 2 # Send to self (using local board ID)\n", + " \n", + " # Configure XCOM for loopback\n", + " soc.xcom_0.xcom_cfg = 2 + 0x100 # Loopback mode\n", + " \n", + " # Clear destination memory\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_WRITE_MEM'], dest, 0)\n", + " \n", + " data_in = []\n", + " start_time = time.time()\n", + " \n", + " for i in range(1, num_transfers):\n", + " # Send 16-bit value\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_SEND_16BIT_2'], dest, i)\n", + " \n", + " # Wait for data to be received\n", + " while soc.xcom_0.mem == 0:\n", + " pass\n", + " \n", + " data_in.append(soc.xcom_0.mem)\n", + " \n", + " # Verify data integrity\n", + " if soc.xcom_0.mem != i:\n", + " print(f\"Data mismatch at i={i}: sent {i}, received {soc.xcom_0.mem}\")\n", + " \n", + " # Clear for next transfer\n", + " soc.xcom_0.run_cmd(soc.xcom_0.opcodes['XCOM_WRITE_MEM'], dest, 0)\n", + " \n", + " elapsed = time.time() - start_time\n", + " return data_in, elapsed\n", + "\n", + "# Run loopback test\n", + "print(\"Running XCOM loopback test...\")\n", + "try:\n", + " data_received, elapsed_time = xcom_loopback_test(soc, num_transfers=500)\n", + "\n", + " print(f\"\\nLoopback test complete.\")\n", + " print(f\"Elapsed time: {elapsed_time:.3f}s\")\n", + " print(f\"Transfers: {len(data_received)}\")\n", + " if len(data_received) > 0:\n", + " print(f\"Average latency per transfer: {elapsed_time/len(data_received)*1000:.3f} ms\")\n", + " \n", + " # Plot received data to verify ramp pattern\n", + " plt.figure(figsize=(12, 6))\n", + " if len(data_received) > 100:\n", + " plt.plot(data_received[0:100], '.-')\n", + " else:\n", + " plt.plot(data_received, '.-')\n", + " plt.xlabel('Transfer Number')\n", + " plt.ylabel('Received Value')\n", + " plt.title('XCOM Loopback Test - Received Data')\n", + " plt.grid(True)\n", + " plt.show()\n", + "except Exception as e:\n", + " print(f\"Loopback test failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 9. Latency Measurement\n", + "\n", + "XCOM provides deterministic, low-latency communication. Precise latency can be measured using the tProc's absolute time counter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Assembly program to measure XCOM round-trip latency\n", + "latency_measure_asm = \"\"\"\n", + "// XCOM Latency Measurement Program\n", + "// Measures round-trip latency by timing a send-receive cycle\n", + "\n", + "// Initialize registers\n", + "REG_WR r1 imm #1\n", + "REG_WR r2 imm #2 // Destination board ID\n", + "REG_WR r3 imm #3\n", + "REG_WR r4 imm #4\n", + "REG_WR r5 imm #5\n", + "REG_WR r10 imm #0 // DMEM pointer for data storage\n", + "\n", + "// Write some zeros to DMEM\n", + "DMEM_WR [r10] imm #0\n", + "DMEM_WR [r10 + &1] imm #0\n", + "DMEM_WR [r10 + &2] imm #0\n", + "DMEM_WR [r10 + &3] imm #0\n", + "DMEM_WR [r10 + &4] imm #0\n", + "DMEM_WR [r10 + &5] imm #0\n", + "\n", + "// Clear XCOM flag\n", + "REG_WR s_ctrl imm ctrl_csf_qpa\n", + "\n", + "LOOP:\n", + " // Record start time\n", + " REG_WR r12 op -op(s_usr_time)\n", + " \n", + " // Send data via XCOM\n", + " WAIT qpa_rdy\n", + " PA 6 r2 r6 // XCOM QCTRL operation\n", + " WAIT qpa_dt\n", + " \n", + " // Wait for response (flag from destination)\n", + " WAIT qpa_rdy\n", + " PA 0 r2 // Clear flag\n", + " WAIT qpa_dt\n", + " \n", + " // Record end time\n", + " REG_WR r13 op -op(s_usr_time)\n", + " \n", + " // Store delta time in DMEM\n", + " DMEM_WR [r10] op -op(r13 - r12)\n", + " \n", + " // Increment pointer and data\n", + " REG_WR r6 op -op(r6 + #1)\n", + " REG_WR r10 op -op(r10 + #1)\n", + " \n", + " // Loop counter\n", + " REG_WR r11 op -op(r11 - #1) -uf\n", + " JUMP LOOP -if(NZ)\n", + "\n", + ".END\n", + "\"\"\"\n", + "\n", + "print(\"Latency measurement program ready.\")\n", + "print(\"Run this on the master board to measure XCOM round-trip latency.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 9.1 Analyzing Latency Data\n", + "\n", + "After running the latency measurement program, analyze the collected timing data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Example: Reading latency data from DMEM after tProc execution\n", + "def read_latency_data(soc, num_samples=100):\n", + " \"\"\"Read latency measurement data from tProc DMEM.\"\"\"\n", + " delta_times = []\n", + " \n", + " for i in range(num_samples):\n", + " # Read from DMEM (addresses 0, 1, 2, ...)\n", + " # Method name may vary based on firmware\n", + " # delta = soc.qick_processor_0.single_read(2, i)\n", + " delta_times.append(0) # Placeholder\n", + " \n", + " return delta_times\n", + "\n", + "print(\"Latency data analysis functions defined.\")\n", + "print(\"\\nAfter running the tProc program, you would analyze data with:\")\n", + "print(\" delta_times = read_latency_data(soc, num_samples=100)\")\n", + "print(\" plt.hist(delta_times, bins=50)\")\n", + "print(\" print(f'Average latency: {np.mean(delta_times)} cycles')\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 10. Using Flags for Synchronization\n", + "\n", + "XCOM flags provide a fast, single-bit signaling mechanism between boards. They are useful for:\n", + "- Triggering actions on slave boards\n", + "- Synchronizing state machines across boards\n", + "- Implementing token ring or handshake protocols" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set flag on another board\n", + "def set_remote_flag(soc, dest_board_id):\n", + " \"\"\"Set the flag on a remote board.\"\"\"\n", + " print(f\"Setting flag on board {dest_board_id}...\")\n", + " soc.xcom_0.set_flag(dest_board_id)\n", + " print(\"Flag set.\")\n", + "\n", + "# Clear flag on another board\n", + "def clear_remote_flag(soc, dest_board_id):\n", + " \"\"\"Clear the flag on a remote board.\"\"\"\n", + " print(f\"Clearing flag on board {dest_board_id}...\")\n", + " soc.xcom_0.clear_flag(dest_board_id)\n", + " print(\"Flag cleared.\")\n", + "\n", + "# Check local flag status\n", + "def check_local_flag(soc):\n", + " \"\"\"Check the status of the local flag.\"\"\"\n", + " flag_status = soc.xcom_0.flag\n", + " print(f\"Local flag: {flag_status}\")\n", + " return flag_status\n", + "\n", + "# From tProc, you can wait for a flag using WAIT instruction\n", + "# WAIT qpa_rdy\n", + "# PA 0 r2 # Check/clear flag\n", + "# WAIT qpa_dt\n", + "\n", + "print(\"Flag operations defined.\")\n", + "print(\"\\nExample usage:\")\n", + "print(\" set_remote_flag(soc_master, slave_id)\")\n", + "print(\" check_local_flag(soc_slave)\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 11. Absolute Clock Synchronization\n", + "\n", + "One of XCOM's most powerful features is **absolute clock synchronization** across all boards. The master can broadcast reset and start commands to align all tProc absolute time counters.\n", + "\n", + "### 11.1 Synchronization Protocol\n", + "\n", + "1. **Frequency synchronization**: All boards share the same external reference clock (e.g., 100 MHz rubidium)\n", + "2. **Phase synchronization**: PLLs configured in nested zero-delay mode\n", + "3. **DAC/ADC alignment**: Multi-tile sync (MTS) calibration\n", + "4. **Absolute time alignment**: Master broadcasts RESET and START commands" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Master broadcasts reset to all boards\n", + "def broadcast_reset(soc_master):\n", + " \"\"\"Broadcast absolute clock reset to all boards.\"\"\"\n", + " print(\"Broadcasting RESET command to all boards...\")\n", + " \n", + " # Use XCOM_QRST_SYNC opcode to broadcast reset\n", + " soc_master.xcom_0.run_cmd(soc_master.xcom_0.opcodes['XCOM_QRST_SYNC'], 0, 0)\n", + " time.sleep(0.1)\n", + " print(\"RESET broadcast complete.\")\n", + "\n", + "# Master broadcasts start to all boards\n", + "def broadcast_start(soc_master):\n", + " \"\"\"Broadcast absolute clock start to all boards.\"\"\"\n", + " print(\"Broadcasting START command to all boards...\")\n", + " \n", + " # Use XCOM_QCTRL with appropriate parameters to start\n", + " soc_master.xcom_0.run_cmd(soc_master.xcom_0.opcodes['XCOM_QCTRL'], 4, 6) # proc_start\n", + " time.sleep(0.1)\n", + " print(\"START broadcast complete.\")\n", + "\n", + "# Verify synchronization\n", + "def verify_sync(soc_board, expected_time=None):\n", + " \"\"\"Read absolute time from tProc to verify sync.\"\"\"\n", + " # Read absolute time from tProc (address depends on firmware)\n", + " # This is a placeholder - actual register address may vary\n", + " # abs_time = soc_board.qick_processor_0.single_read(2, 0)\n", + " abs_time = 0 # Placeholder\n", + " print(f\"Board absolute time: {abs_time}\")\n", + " return abs_time\n", + "\n", + "print(\"Synchronization functions defined.\")\n", + "print(\"\\nTypical synchronization sequence:\")\n", + "print(\" 1. broadcast_reset(soc_master) # Reset all absolute clocks\")\n", + "print(\" 2. time.sleep(0.1)\")\n", + "print(\" 3. broadcast_start(soc_master) # Start all absolute clocks\")\n", + "print(\" 4. verify_sync(soc_master)\")\n", + "print(\" 5. verify_sync(soc_slave1) # Should show same value\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 12. Debugging XCOM\n", + "\n", + "XCOM provides several debugging registers to help diagnose issues." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def diagnose_xcom(soc):\n", + " \"\"\"Run comprehensive XCOM diagnostics.\"\"\"\n", + " print(\"=\" * 60)\n", + " print(\"XCOM DIAGNOSTICS\")\n", + " print(\"=\" * 60)\n", + " \n", + " # Check XCOM presence\n", + " if not hasattr(soc, 'xcom_0'):\n", + " print(\"❌ XCOM not found! Check firmware bitstream.\")\n", + " return False\n", + " print(\"✅ XCOM found\")\n", + " \n", + " # Print status register\n", + " print(\"\\n--- Status Register ---\")\n", + " soc.xcom_0.print_status()\n", + " \n", + " # Print debug register\n", + " print(\"\\n--- Debug Register ---\")\n", + " soc.xcom_0.print_debug()\n", + " \n", + " # Print all registers\n", + " print(\"\\n--- All Registers ---\")\n", + " soc.xcom_0.print_axi_regs()\n", + " \n", + " # Test basic read/write\n", + " print(\"\\n--- Basic Read/Write Test ---\")\n", + " test_addr = 5\n", + " test_value = 0x12345678\n", + " \n", + " try:\n", + " soc.xcom_0.axi_addr = test_addr\n", + " original = soc.xcom_0.mem\n", + " soc.xcom_0.mem = test_value\n", + " readback = soc.xcom_0.mem\n", + " soc.xcom_0.mem = original # Restore\n", + " \n", + " if readback == test_value:\n", + " print(f\"✅ Memory write/read: wrote 0x{test_value:08X}, read 0x{readback:08X}\")\n", + " else:\n", + " print(f\"❌ Memory mismatch: wrote 0x{test_value:08X}, read 0x{readback:08X}\")\n", + " except Exception as e:\n", + " print(f\"❌ Memory test failed: {e}\")\n", + " \n", + " # Test loopback (if configured)\n", + " print(\"\\n--- Loopback Test ---\")\n", + " try:\n", + " data, elapsed = xcom_loopback_test(soc, num_transfers=10)\n", + " if len(data) == 9 and all(data[i] == i+1 for i in range(9)):\n", + " print(f\"✅ Loopback test passed ({len(data)} transfers)\")\n", + " else:\n", + " print(f\"⚠️ Loopback test had {len(data)} transfers, expected 9\")\n", + " except Exception as e:\n", + " print(f\"❌ Loopback test failed: {e}\")\n", + " \n", + " print(\"\\n\" + \"=\" * 60)\n", + " return True\n", + "\n", + "# Run diagnostics\n", + "diagnose_xcom(soc)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 13. Common Issues and Troubleshooting\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"\"\"\n", + "╔══════════════════════════════════════════════════════════════════════════════════════════════════════════════╗\n", + "║ XCOM TROUBLESHOOTING GUIDE ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ Issue │ Likely Cause │ Solution ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ XCOM not detected │ Wrong firmware │ Use bitstream with XCOM IP ║\n", + "║ │ Missing FMC board │ Install XCOM transceiver board ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ AUTO_ID fails │ No external hub connected │ Connect all boards to XCOM hub ║\n", + "║ │ Cable mismatch │ Use matched-length LVDS cables ║\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ High latency (>200 ns) │ XCOM clock too low │ Increase LVDS clock (up to 312.9 MHz) │\n", + "║ │ Software overhead │ Use tProc instead of Python for timing │\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ Data corruption │ LVDS signal integrity │ Check cable quality and length │\n", + "║ │ Clock skew │ Run MTS calibration │\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ Flag not received │ Wrong destination ID │ Verify board IDs with auto_id() │\n", + "║ │ Flag not cleared │ Always clear flag after receipt │\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ tProc XCOM commands fail │ Peripheral not ready │ Add WAIT qpa_rdy before PA │\n", + "║ │ Incorrect PA configuration │ Verify cfg register values │\n", + "║ ──────────────────────────────┼─────────────────────────────────┼───────────────────────────────────────────║\n", + "║ Synchronization drift │ External clock not locked │ Verify PLL lock status │\n", + "║ │ Missing MTS calibration │ Run init_tile_sync() and sync_tiles() │\n", + "║ ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ QUICK DIAGNOSTIC COMMANDS ║\n", + "╠══════════════════════════════════════════════════════════════════════════════════════════════════════════════╣\n", + "║ ║\n", + "║ # Check if XCOM is present ║\n", + "║ hasattr(soc, 'xcom_0') ║\n", + "║ ║\n", + "║ # Print all XCOM registers ║\n", + "║ soc.xcom_0.print_axi_regs() ║\n", + "║ ║\n", + "║ # Check XCOM status ║\n", + "║ soc.xcom_0.print_status() ║\n", + "║ ║\n", + "║ # Run full diagnostics ║\n", + "║ diagnose_xcom(soc) ║\n", + "║ ║\n", + "╚══════════════════════════════════════════════════════════════════════════════════════════════════════════════╝\n", + "\"\"\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 14. Summary\n", + "\n", + "You have learned:\n", + "\n", + "1. **XCOM Architecture**: Full mesh network for multi-board synchronization and communication\n", + "2. **Hardware Requirements**: FMC transceiver boards and external hub for 5-15 boards\n", + "3. **Network Configuration**: AUTO_ID, set_local_id, master/slave roles\n", + "4. **Data Transfer**: Sending 8/16/32-bit data between boards with deterministic latency\n", + "5. **tProc Integration**: Using PA instruction for real-time XCOM access\n", + "6. **Flags**: Fast single-bit signaling for synchronization\n", + "7. **Absolute Clock Sync**: Broadcasting RESET/START to align all tProcs\n", + "8. **Debugging**: Status registers, diagnostics, troubleshooting\n", + "\n", + "### Key Takeaways\n", + "\n", + "- XCOM enables <100 ps skew and 186 ns deterministic latency between boards\n", + "- Each board has a dedicated Tx channel; all boards listen on all Rx channels\n", + "- The master board can synchronize absolute clocks across the entire network\n", + "- tProc integration allows real-time XCOM communication without software overhead\n", + "- Always run MTS calibration and verify external clock before using XCOM\n", + "\n", + "### Next Steps\n", + "\n", + "- For simpler multi-board synchronization (external clock + external start), see [`10_Multi_Board_Synchronization.ipynb`](./10_Multi_Board_Synchronization.ipynb)\n", + "- Proceed to explore other advanced topics\n", + "- For more information, refer to the XCOM paper (arXiv:2603.18977v1)\n", + "- Check the QICK firmware documentation for XCOM IP details\n", + "\n", + "## References\n", + "\n", + "- For detailed XCOM documentation, see [XCOM Overview](../topics/xcom.rst)\n", + "- XCOM Command Reference: [Command Reference](../topics/XCOM-commands.rst)\n", + "- XCOM Paper: [arXiv:2603.18977v1](https://arxiv.org/abs/2603.18977v1)\n", + "- QICK XCOM driver: `qick.ip.QICK_Xcom`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.8.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/15_QICKBox_RF_Daughtercards.ipynb b/docs/source/tutorials/15_QICKBox_RF_Daughtercards.ipynb new file mode 100644 index 000000000..80bf75a05 --- /dev/null +++ b/docs/source/tutorials/15_QICKBox_RF_Daughtercards.ipynb @@ -0,0 +1,551 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 15 - QICKBox RF Daughtercards\n", + "\n", + "**Objective:** Learn how to use the QICKBox's RF and Balun daughtercards: setting the digitally\n", + "controlled attenuators, and shaping the signal chain with the ADMV8818 tunable RF filters. This\n", + "notebook covers:\n", + "- What the QICKBox is and how its daughtercards map to DAC/ADC channels\n", + "- Setting DAC/ADC attenuators on the RF and Balun daughtercards\n", + "- Bypassing, sweeping, and combining the ADMV8818 low-pass/high-pass filter bands\n", + "- Measuring signal power to check for saturation\n", + "\n", + "**Prerequisites:**\n", + "- A QICKBox with an RF-out and RF-in daughtercard (and, optionally, Balun-out/Balun-in daughtercards)\n", + " installed, wired for loopback per the diagram below\n", + "- Completed basic tutorials (00-05) and [`06_Generators_And_Readouts.ipynb`](../tutorials/06_Generators_And_Readouts.ipynb)\n", + " for generator/readout/pulse concepts, which are not re-explained here\n", + "- Firmware bitstream built for the QICKBox variant of your board (`RFQickSoc216V1` or equivalent)\n", + "\n", + "**References:**\n", + "- QICK Documentation: https://docs.qick.dev\n", + "- Firmware overview: [Firmware Overview](../firmware/index.rst)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Overview\n", + "\n", + "The QICKBox is a QICK carrier board that breaks DAC/ADC channels out to swappable RF daughtercards,\n", + "instead of driving SMAs directly off the RFSoC. Two daughtercard types matter here:\n", + "\n", + "- **RF daughtercards** (RF-out / RF-in): chains of amplifiers and digitally controlled attenuators\n", + " tuned to drive a fridge directly, with no additional warm amplification. They also carry an\n", + " **ADMV8818** digitally tunable filter, used to remove spurious Nyquist images on the DAC side and\n", + " Nyquist-folded noise on the ADC side.\n", + "- **Balun daughtercards** (Balun-out / Balun-in): a simpler passive path (no attenuators, no filter),\n", + " useful as a reference/loopback channel or for signals that don't need the RF chain's gain and filtering.\n", + "\n", + "Each daughtercard slot exposes several physical DAC/ADC ports; which *firmware* generator/readout\n", + "channel a given port maps to depends on the bitstream build.\n", + "\n", + "### Wiring for this notebook\n", + "\n", + "```\n", + "\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n", + "\u2502 QICKBox Loopback Wiring \u2502\n", + "\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n", + "\u2502 DAC slot 1: RF-out daughtercard (ports 4-7) \u2502\n", + "\u2502 DAC slot 2: Balun-out daughtercard (ports 8-12) \u2502\n", + "\u2502 ADC slot 2: RF-in daughtercard (ports 4-5) \u2502\n", + "\u2502 ADC slot 3: Balun-in daughtercard (ports 6-7) \u2502\n", + "\u2502 \u2502\n", + "\u2502 DAC port 4 \u2500\u2500[40 dB attenuator]\u2500\u2500\u25ba ADC port 5 \u2502\n", + "\u2502 DAC port 5 \u2500\u2500[40 dB attenuator]\u2500\u2500\u25ba ADC port 4 \u2502\n", + "\u2502 DAC port 8 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u25ba ADC port 6 \u2502\n", + "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n", + "```\n", + "\n", + "**Important notes:**\n", + "- Adjust the port numbers and firmware channel numbers below to match your own daughtercard slots\n", + " and cabling -- the mapping is fixed by the bitstream, not by this notebook.\n", + "- The RF daughtercards' amplifiers can saturate (or clip the ADC) if you drive them with too little\n", + " attenuation. Start with generous attenuation (as in [Section 3](#3.-Daughtercard-Attenuators)) and\n", + " reduce it gradually while watching the readout amplitude.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Setup and Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Standard imports\n", + "%matplotlib inline\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "from tqdm.auto import tqdm\n", + "\n", + "from qick.asm_v2 import AveragerProgramV2, QickSweep1D\n", + "from qick.rfboard import RFQickSoc216V1\n", + "\n", + "# Connect to the board (adjust the path to your firmware)\n", + "BITSTREAM_PATH = 'qick_216_rfbv2.bit' # <--- CHANGE THIS\n", + "soc = RFQickSoc216V1(BITSTREAM_PATH)\n", + "soccfg = soc\n", + "\n", + "# Firmware channel numbers for the generators/readouts wired above\n", + "GEN_CH_RF = 5\n", + "GEN_CH_BALUN = 8\n", + "RO_CH_RF = 0\n", + "RO_CH_BALUN = 1\n", + "\n", + "# Physical DAC/ADC ports on the RF daughtercards (attenuators/filters are\n", + "# addressed by port, not by firmware channel)\n", + "DAC_RF = 5\n", + "ADC_RF = 4\n", + "\n", + "# Starting attenuations, dB (0 through 31.75) -- generous, to avoid\n", + "# saturating the chain before we've characterized it\n", + "DAC_ATT = [0, 10]\n", + "ADC_ATT = 25\n", + "\n", + "print(f\"Firmware: {soc.get_cfg()['fw_version']}\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Daughtercard Attenuators\n", + "\n", + "`rfb_set_dac_rf`/`rfb_set_adc_rf` set the digitally controlled attenuators on an RF daughtercard's\n", + "DAC/ADC port. The DAC side takes two attenuator values (two stages in the chain); the ADC side takes\n", + "one. Let's set some starting attenuations and run a loopback pulse through the RF daughtercard to\n", + "confirm the signal chain is working, using decimated readout so we can look at the pulse shape.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"set DAC attenuators:\", soc.rfb_set_dac_rf(DAC_RF, *DAC_ATT))\n", + "print(\"set ADC attenuators:\", soc.rfb_set_adc_rf(ADC_RF, ADC_ATT))\n", + "\n", + "\n", + "class LoopbackProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " self.declare_gen(ch=gen_ch, nqz=cfg['nqz'], mixer_freq=cfg['mixer_freq'], ro_ch=ro_ch)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + " self.add_readoutconfig(ch=ro_ch, name=\"myro\", freq=cfg['freq'], gen_ch=gen_ch, outsel='product')\n", + " self.add_cosine(ch=gen_ch, name=\"ramp\", length=cfg['ramp_len'], even_length=True)\n", + " self.add_pulse(ch=gen_ch, name=\"mypulse\", ro_ch=ro_ch,\n", + " style=\"flat_top\",\n", + " envelope=\"ramp\",\n", + " freq=cfg['freq'],\n", + " length=cfg['flat_len'],\n", + " phase=cfg['phase'],\n", + " gain=cfg['gain'])\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"myro\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.delay_auto()\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mypulse\", t=0.0)\n", + " self.trigger(ros=[cfg['ro_ch']], t=cfg['trig_time'])\n", + "\n", + "\n", + "config = {'gen_ch': GEN_CH_RF,\n", + " 'ro_ch': RO_CH_RF,\n", + " 'mixer_freq': 6000,\n", + " 'freq': 6000,\n", + " 'nqz': 2,\n", + " 'trig_time': 0.0,\n", + " 'ro_len': 3.0,\n", + " 'flat_len': 1.0,\n", + " 'ramp_len': 1.0,\n", + " 'phase': 0,\n", + " 'gain': 1.0}\n", + "\n", + "# start the RF filters centered on the pulse frequency (Section 4 covers this call in detail)\n", + "soc.rfb_set_dac_filter(DAC_RF, fc=config['freq'] / 1000, ftype='bandpass', bw=1.0)\n", + "soc.rfb_set_adc_filter(ADC_RF, fc=config['freq'] / 1000, ftype='bandpass', bw=1.0)\n", + "\n", + "prog = LoopbackProgram(soccfg, reps=1, final_delay=0.5, cfg=config)\n", + "iq_list = prog.acquire_decimated(soc, rounds=10)\n", + "\n", + "t = prog.get_time_axis(ro_index=0)\n", + "iq = iq_list[0]\n", + "plt.plot(t, iq[:, 0], label=\"I value\")\n", + "plt.plot(t, iq[:, 1], label=\"Q value\")\n", + "plt.plot(t, np.abs(iq.dot((1, 1j))), label=\"magnitude\")\n", + "plt.legend()\n", + "plt.ylabel(\"amplitude [ADU]\")\n", + "plt.xlabel(\"time [us]\");\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Observation:** with the attenuators and filter both centered on the pulse frequency, you should see\n", + "a clean flat-top pulse. If the magnitude trace is clipped/flat-topped, add more attenuation; if it's\n", + "very small and noisy, reduce it.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Daughtercard RF Filters (ADMV8818)\n", + "\n", + "Each RF daughtercard's ADMV8818 filter has four selectable low-pass **bands** and four high-pass\n", + "**bands**, each tunable through 16 **states** -- 3-bit band select + 4-bit state select per filter\n", + "type. `rfb_set_dac_filter`/`rfb_set_adc_filter` wrap this into a `fc`/`bw`/`ftype` (\"bandpass\" or\n", + "\"bypass\") interface. To characterize the filter itself, it's more direct to drive the underlying\n", + "registers, which is what `set_filter` below does.\n", + "\n", + "We measure the filter response as an S21 sweep: play a swept-frequency tone and read back the\n", + "demodulated amplitude at each frequency.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class FreqSweepProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " self.declare_gen(ch=gen_ch, nqz=cfg['nqz'], mixer_freq=cfg['mixer_freq'], ro_ch=ro_ch)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + " self.add_loop(\"myloop\", self.cfg[\"steps\"])\n", + " self.add_readoutconfig(ch=ro_ch, name=\"myro\", freq=cfg['freq'], gen_ch=gen_ch)\n", + " self.add_pulse(ch=gen_ch, name=\"mypulse\", ro_ch=ro_ch,\n", + " style=\"const\",\n", + " freq=cfg['freq'],\n", + " length=cfg['pulse_len'],\n", + " phase=cfg['phase'],\n", + " gain=cfg['gain'])\n", + "\n", + " def _body(self, cfg):\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"myro\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mypulse\", t=0)\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'])\n", + "\n", + "\n", + "def measure_s21(gen_ch, ro_ch, nqz, gain, steps=101, dds_range=0.45, overlap=0, plot=False, progress=True):\n", + " # the interpolated-generator frequency range is limited, so stitch several\n", + " # mixer_freq-centered sweeps together to cover a wide band\n", + " soc.clear_interrupts()\n", + " config = {'steps': steps, 'gen_ch': gen_ch, 'ro_ch': ro_ch, 'nqz': nqz,\n", + " 'trig_time': 0.4, 'pulse_len': 10.0, 'ro_len': 10.1, 'phase': 0, 'gain': gain}\n", + " allfreqs, allpowers = [], []\n", + " f_dds = soccfg['gens'][gen_ch]['f_dds']\n", + " mixer_freqs = np.arange(0.5, 10000 / f_dds, dds_range * 2 - overlap) * f_dds\n", + " for mixer_freq in tqdm(mixer_freqs, disable=not progress):\n", + " config['mixer_freq'] = mixer_freq\n", + " config['freq'] = QickSweep1D(\"myloop\", mixer_freq - dds_range * f_dds, mixer_freq + dds_range * f_dds)\n", + " prog = FreqSweepProgram(soccfg, reps=10, final_delay=1.0, cfg=config)\n", + " freqs = prog.get_pulse_param('myro', 'freq', as_array=True)\n", + " iq_list = prog.acquire(soc, rounds=1, progress=False)\n", + " powers = 20 * np.log10(np.abs(iq_list[0][0].dot([1, 1j])))\n", + " allfreqs.append(freqs)\n", + " allpowers.append(powers)\n", + " if plot:\n", + " plt.plot(freqs, powers, label=\"mixer_freq=%f\" % (mixer_freq))\n", + " return np.array(allfreqs).flatten(), np.array(allpowers).flatten()\n", + "\n", + "\n", + "def set_filter(lpf, hpf, filt=0):\n", + " \"\"\"Drive the ADMV8818 band-select (lpf/hpf) and state (filt) registers directly,\n", + " on both the RF-in and RF-out daughtercards.\"\"\"\n", + " sw = 0xc0 + (hpf << 3) + lpf\n", + " filt_bits = (filt << 4) + filt\n", + " rfb_ch = soc.adc_chains[ADC_RF]\n", + " with soc.board_sel.enable_context(rfb_ch.card_num):\n", + " rfb_ch.filter.write_reg('WR0_SW', sw)\n", + " rfb_ch.filter.write_reg('WR0_FILTER', filt_bits)\n", + " rfb_ch = soc.dac_chains[DAC_RF]\n", + " with soc.board_sel.enable_context(rfb_ch.card_num):\n", + " rfb_ch.filter.write_reg('WR0_SW', sw)\n", + " rfb_ch.filter.write_reg('WR0_FILTER', filt_bits)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Bypass mode\n", + "\n", + "With the filters in bypass mode there's no rejection of Nyquist images/aliases, so we need to crank up\n", + "all of the attenuators to avoid saturating the chain.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "print(\"set DAC attenuators:\", soc.rfb_set_dac_rf(DAC_RF, 30, 30))\n", + "print(\"set ADC attenuators:\", soc.rfb_set_adc_rf(ADC_RF, 30))\n", + "soc.rfb_set_rfadc_attenuator(ADC_RF, 10)\n", + "soc.rfb_set_dac_filter(DAC_RF, fc=0, ftype='bypass')\n", + "soc.rfb_set_adc_filter(ADC_RF, fc=0, ftype='bypass')\n", + "\n", + "plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, nqz=2, gain=1.0, steps=501, dds_range=0.45, overlap=0.1))\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.title(\"bypass mode\");\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Sweeping bands and states\n", + "\n", + "Let's work our way through the low-pass and high-pass filter banks: first the four bands (at a fixed\n", + "state), then all 16 states within one band.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for lpf in tqdm(range(0, 5)):\n", + " set_filter(lpf, 0, 0)\n", + " plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, 2, 1.0, progress=False), label=\"lpf=%d\" % (lpf))\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.title(\"LPF bands, state=0\")\n", + "plt.legend();\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for state in tqdm(range(0, 16)):\n", + " set_filter(2, 0, state)\n", + " plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, 2, 1.0, progress=False), label=\"state=%d\" % (state))\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.title(\"LPF states, band=2\")\n", + "plt.legend();\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for hpf in tqdm(range(0, 5)):\n", + " set_filter(0, hpf, 0)\n", + " plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, 2, 1.0, progress=False), label=\"hpf=%d\" % (hpf))\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.title(\"HPF bands, state=0\")\n", + "plt.legend();\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for state in tqdm(range(0, 16)):\n", + " set_filter(0, 2, state)\n", + " plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, 2, 1.0, progress=False), label=\"state=%d\" % (state))\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.title(\"HPF states, band=2\")\n", + "plt.legend();\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Combining LPF + HPF into a bandpass\n", + "\n", + "This is what `rfb_set_dac_filter`/`rfb_set_adc_filter` with `ftype='bandpass'` do for you: they pick a\n", + "matching LPF/HPF band+state pair to approximate the requested center frequency `fc` and bandwidth `bw`.\n", + "As the passband narrows, more attenuation can be removed for the same signal level -- the filter itself\n", + "is doing some of the rejection work that attenuation was doing in bypass mode.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "for fc, bw, dac_att2, adc_att in [(5, 1.0, 30, 30), (5.5, 0.5, 20, 25), (6, 0.5, 20, 25), (7, 1.0, 15, 20)]:\n", + " soc.rfb_set_dac_filter(DAC_RF, fc=fc, ftype='bandpass', bw=bw)\n", + " soc.rfb_set_adc_filter(ADC_RF, fc=fc, ftype='bandpass', bw=bw)\n", + " soc.rfb_set_dac_rf(DAC_RF, 0, dac_att2)\n", + " soc.rfb_set_adc_rf(ADC_RF, adc_att)\n", + " plt.plot(*measure_s21(GEN_CH_RF, RO_CH_RF, nqz=2, gain=1.0, steps=501, dds_range=0.45, overlap=0.1),\n", + " label=\"fc=%.1f, bw=%.1f\" % (fc, bw))\n", + "\n", + "# leave the chain in a safe, heavily attenuated state\n", + "soc.rfb_set_dac_rf(DAC_RF, 30, 30)\n", + "soc.rfb_set_adc_rf(ADC_RF, 30)\n", + "\n", + "plt.ylabel(\"S21 [arb. dB]\")\n", + "plt.xlabel(\"Frequency [MHz]\")\n", + "plt.ylim(bottom=0)\n", + "plt.xlim((3000, 9000))\n", + "plt.legend();\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Measuring Power and Checking for Saturation\n", + "\n", + "When you change gain, attenuation, or filtering, it's useful to have a quick way to measure the\n", + "resulting readout amplitude -- both to compare configurations and to check you haven't saturated the\n", + "DAC output stage or the ADC input. `PeriodicProgram` plays a continuous tone so the readout amplitude\n", + "converges to a steady-state value; `measure_gain` wraps it into a one-line power measurement for a\n", + "given generator gain and pair of attenuator settings.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class PeriodicProgram(AveragerProgramV2):\n", + " def _initialize(self, cfg):\n", + " ro_ch = cfg['ro_ch']\n", + " gen_ch = cfg['gen_ch']\n", + " self.declare_gen(ch=gen_ch, nqz=cfg['nqz'], mixer_freq=cfg['mixer_freq'], ro_ch=ro_ch)\n", + " self.declare_readout(ch=ro_ch, length=cfg['ro_len'])\n", + " self.add_readoutconfig(ch=ro_ch, name=\"myro\", freq=cfg['freq'], gen_ch=gen_ch, outsel='product')\n", + " self.add_pulse(ch=gen_ch, name=\"mypulse\", ro_ch=ro_ch,\n", + " style=\"const\",\n", + " freq=cfg['freq'],\n", + " length=cfg['flat_len'],\n", + " phase=cfg['phase'],\n", + " gain=cfg['gain'],\n", + " mode='periodic')\n", + " self.send_readoutconfig(ch=cfg['ro_ch'], name=\"myro\", t=0)\n", + " self.pulse(ch=cfg['gen_ch'], name=\"mypulse\", t=0)\n", + "\n", + " def _body(self, cfg):\n", + " self.trigger(ros=[cfg['ro_ch']], pins=[0], t=cfg['trig_time'], mr=True)\n", + "\n", + "\n", + "def measure_gain(gain, dac_att1, dac_att2, adc_att):\n", + " config = {'gen_ch': GEN_CH_RF, 'ro_ch': RO_CH_RF, 'mixer_freq': 6000, 'freq': 6000, 'nqz': 2,\n", + " 'trig_time': 0.0, 'ro_len': 10.0, 'flat_len': 1.0, 'phase': 0, 'gain': gain}\n", + " soc.rfb_set_dac_rf(DAC_RF, dac_att1, dac_att2)\n", + " soc.rfb_set_adc_rf(ADC_RF, adc_att)\n", + " prog = PeriodicProgram(soccfg, reps=1000, final_delay=0.5, cfg=config)\n", + " mag = np.abs(prog.acquire(soc, progress=False)[0][0].dot([1, 1j]))\n", + " soc.reset_gens()\n", + " soc.clear_interrupts()\n", + " return mag\n", + "\n", + "\n", + "# baseline, then compare a few knobs against it, each as a dB change\n", + "ref_mag = measure_gain(0.5, DAC_ATT[0], DAC_ATT[1], ADC_ATT)\n", + "print(\"reference amplitude: %f\" % (ref_mag))\n", + "\n", + "mag = measure_gain(0.5, DAC_ATT[0], DAC_ATT[1] - 3, ADC_ATT)\n", + "print(\"with -3 dB DAC att2: amplitude %f, a %.2f dB change\" % (mag, 20 * np.log10(mag / ref_mag)))\n", + "\n", + "mag = measure_gain(0.5, DAC_ATT[0], DAC_ATT[1], ADC_ATT - 3)\n", + "print(\"with -3 dB ADC att: amplitude %f, a %.2f dB change\" % (mag, 20 * np.log10(mag / ref_mag)))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Watch for saturation:** if the measured amplitude stops scaling linearly as you reduce attenuation\n", + "(dB change stops matching the attenuator step), you've saturated the DAC output amp, the ADC front end,\n", + "or both -- back off and re-measure.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Summary\n", + "\n", + "You have learned:\n", + "\n", + "1. **QICKBox daughtercards**: RF daughtercards (amplifiers + attenuators + ADMV8818 filter) vs.\n", + " Balun daughtercards (simple passive path), and how physical ports differ from firmware channels\n", + "2. **Attenuators**: `rfb_set_dac_rf`/`rfb_set_adc_rf` to set per-stage attenuation on a daughtercard port\n", + "3. **ADMV8818 filters**: bypass mode, sweeping LPF/HPF bands and states, and combining them into a\n", + " bandpass response via `rfb_set_dac_filter`/`rfb_set_adc_filter`\n", + "4. **Saturation checking**: using a periodic tone and `measure_gain` to confirm gain/attenuation changes\n", + " are still in the linear regime\n", + "\n", + "### Key Takeaways\n", + "\n", + "- Filtering and attenuation trade off: a narrower bandpass filter rejects more out-of-band noise/images,\n", + " letting you run with less attenuation for the same in-band signal level\n", + "- Always start a new configuration with generous attenuation and reduce it while watching the readout\n", + " amplitude, rather than guessing a low value up front\n", + "- Balun daughtercards are the simplest way to get a reference/loopback channel without the RF chain's\n", + " gain and filtering in the way\n", + "\n", + "### Next Steps\n", + "\n", + "- Return to [`07_Advanced_Generators_And_Readouts.ipynb`](./07_Advanced_Generators_And_Readouts.ipynb)\n", + " for multiplexed generators/readouts and the PFB readout channelizer, which apply equally to signals\n", + " routed through these daughtercards\n", + "- See the [Firmware Overview](../firmware/index.rst) for how daughtercard ports map to generator/readout\n", + " channels in a given bitstream\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/tutorials/README.rst b/docs/source/tutorials/README.rst new file mode 100644 index 000000000..0b48a8eea --- /dev/null +++ b/docs/source/tutorials/README.rst @@ -0,0 +1,267 @@ +========================== +QICK Tutorials (Jupyter) +========================== + +This section contains practical tutorials for learning QICK and the tProcessor control core. +The tutorials are available as Jupyter notebooks and can be run directly on the RFSoC board. + +Prerequisites +============= + +- RFSoC board (ZCU111, ZCU216, or RFSoC4x2) with firmware loaded +- QICK installed (see :doc:`../quick_start`) +- Firmware bitstream file (`.bit` and its matching `.hwh`) for your board +- Access to Jupyter notebook on the board + +List of Tutorials: tProc v2 +=========================== + +These notebooks must be run with a tProc v2 firmware. +Some notebooks may have additional firmware or hardware requirements: + +* Notebooks 00–09 are self-contained and work on any QICK setup +* Notebooks 10–15 require additional hardware resources (multi-board, streaming, DSP48, QICKBox) +* Notebook 13 requires a licensed Vivado installation for custom firmware compilation +* Notebook 14 (XCOM) requires additional hardware: FMC transceiver board and external hub +* Notebook 15 requires a QICKBox with RF (and optionally Balun) daughtercards installed + +Basic Tutorials +--------------- + +.. toctree:: + :maxdepth: 1 + :hidden: + + 00_Getting_Started + 01_Basic_Sequencing + 02_Parameter_Sweeps + 03_Advanced_Timing + 04_Real_Time_Feedback + 05_Dynamic_Parameters_Subroutines + +.. list-table:: + :header-rows: 1 + + * - # + - Notebook + - Description + * - 00 + - :doc:`00_Getting_Started` + - Connect to the board, inspect hardware configuration + * - 01 + - :doc:`01_Basic_Sequencing` + - Define pulses, create sequences, acquire data + * - 02 + - :doc:`02_Parameter_Sweeps` + - 1D and 2D parameter sweeps + * - 03 + - :doc:`03_Advanced_Timing` + - ``delay`` vs ``wait``, auto-timing, avoiding collisions + * - 04 + - :doc:`04_Real_Time_Feedback` + - Conditional pulses, active reset, thresholding + * - 05 + - :doc:`05_Dynamic_Parameters_Subroutines` + - Virtual-Z gates, subroutines, dynamic updates + +Intermediate Tutorials +---------------------- + +.. toctree:: + :maxdepth: 1 + :hidden: + + 06_Generators_And_Readouts + 07_Advanced_Generators_And_Readouts + 08_Hardware_Buffers + 09_Appendix_Tips_And_Limits + +.. list-table:: + :header-rows: 1 + + * - # + - Notebook + - Description + * - 06 + - :doc:`06_Generators_And_Readouts` + - Basic waveform generators, readout resonators, I/Q mixing + * - 07 + - :doc:`07_Advanced_Generators_And_Readouts` + - Muxed generators, PFB readout, dynamic readouts, fast dynamic readout, electrical delay calibration, IQ offsets, performance benchmarks + * - 08 + - :doc:`08_Hardware_Buffers` + - DDR4 and MR buffers, data capture + * - 09 + - :doc:`09_Appendix_Tips_And_Limits` + - Common errors, limits, debugging tips + +Advanced Tutorials +------------------ + +.. toctree:: + :maxdepth: 1 + :hidden: + + 10_Multi_Board_Synchronization + 11_Streaming_And_RealTime_Processing + 12_DSP_Blocks_And_Correlators + 13_Custom_Firmware_Integration + 14_XCOM_Network_Synchronization + 15_QICKBox_RF_Daughtercards + +.. list-table:: + :header-rows: 1 + + * - # + - Notebook + - Description + * - 10 + - :doc:`10_Multi_Board_Synchronization` + - Synchronize multiple boards using external clock and external start signals + * - 11 + - :doc:`11_Streaming_And_RealTime_Processing` + - IQ streaming, on-FPGA averaging, real-time decimation + * - 12 + - :doc:`12_DSP_Blocks_And_Correlators` + - FIR filters, DDS tuning, hardware correlators + * - 13 + - :doc:`13_Custom_Firmware_Integration` + - Adding custom Verilog/VHDL, AXI-lite interface, rebuilding + * - 14 + - :doc:`14_XCOM_Network_Synchronization` + - Full mesh network for multi-board synchronization and low-latency communication (requires FMC transceiver board) + * - 15 + - :doc:`15_QICKBox_RF_Daughtercards` + - QICKBox RF/Balun daughtercards: attenuators, ADMV8818 tunable filters, saturation checks (requires QICKBox with RF daughtercards) + +List of Tutorials: tProc v1 +=========================== + +These notebooks will run on the "standard" tProc v1-based firmware bitstream that is distributed with the QICK library. + +.. toctree:: + :maxdepth: 1 + :hidden: + + tprocv1/00_Send_receive_pulse + tprocv1/01_Phase_coherent_readout + tprocv1/02_Sweeping_variables + tprocv1/03_Conditional_logic + tprocv1/04_Reading_Math_Writing + tprocv1/05_PhaseCoherence_QickProgram + tprocv1/06_qubit_demos + tprocv1/07_Sweep_ND_variables + tprocv1/08_Special_buffers + tprocv1/09_photon_counting + +.. list-table:: + :header-rows: 1 + + * - # + - Notebook + * - 00 + - :doc:`tprocv1/00_Send_receive_pulse` + * - 01 + - :doc:`tprocv1/01_Phase_coherent_readout` + * - 02 + - :doc:`tprocv1/02_Sweeping_variables` + * - 03 + - :doc:`tprocv1/03_Conditional_logic` + * - 04 + - :doc:`tprocv1/04_Reading_Math_Writing` + * - 05 + - :doc:`tprocv1/05_PhaseCoherence_QickProgram` + * - 06 + - :doc:`tprocv1/06_qubit_demos` + * - 07 + - :doc:`tprocv1/07_Sweep_ND_variables` + * - 08 + - :doc:`tprocv1/08_Special_buffers` + * - 09 + - :doc:`tprocv1/09_photon_counting` + +Running the Tutorials +===================== + +1. Connect to your RFSoC board via SSH or JupyterHub +2. Navigate to the ``docs/source/tutorials/`` directory +3. Open the desired notebook +4. **IMPORTANT**: Change the ``BITSTREAM_PATH`` variable in the first cell to point to your firmware file +5. Run the cells in order + +.. note:: + + Don't have an RFSoC board handy? Tutorials 00-07 for tProc v2 are also available + ported to the **QICK emulator** (``QickEmu``, a Verilator-based + software simulation of the firmware) at + ``emulator/notebooks/tutorial/*_emu.ipynb`` in the + `qick repository `_, + and can be run **directly in Google Colab** with no local install -- + each notebook's first "Colab Setup" cell clones the repo and installs + Verilator for you. See the emulator's + `README `_ + for details. Each emulator notebook's intro cell notes what had to be + adapted or skipped versus the hardware original. + +Remote Execution +================ + +.. TODO: needs to explain starting the nameserver; I also suspect this is using make_proxy incorrectly and losing the soccfg object + we should update the pyro notebook and add that in here + +For remote execution (e.g., from a PC), you'll need to start the QICK proxy server on the board first: + +.. code-block:: bash + + python -m qick.pyro + +Then in your notebook, use: + +.. code-block:: python + + from qick.pyro import make_proxy + soc = make_proxy("board_ip_address") + +Common Setup Cell (copy this to any notebook) +============================================= + +.. code-block:: python + + # Configuration + BITSTREAM_PATH = "/path/to/your/firmware.bit" # ← CHANGE THIS + + # Optional: remote execution + USE_PROXY = False + PROXY_IP = "192.168.1.100" + + # Connection + from qick import QickSoc + import numpy as np + import matplotlib.pyplot as plt + + if USE_PROXY: + from qick.pyro import make_proxy + soc = make_proxy(PROXY_IP) + print(f"Connected to proxy at {PROXY_IP}") + else: + soc = QickSoc(bitfile=BITSTREAM_PATH) + print("Connected directly to RFSoC") + + print(f"Firmware: {soc.get_cfg()['fw_version']}") + print(f"tProc cores: {soc.num_tprocs}") + +Troubleshooting +=============== + +* If you see "Bitstream not found", check `BITSTREAM_PATH` +* If tProc commands fail, try `soc.reset_gens()` and `soc.reset_adcs()` +* For proxy connection issues, verify the board IP and that the proxy is running +* If no signal in readout, check DAC→ADC loopback cables, verify `gen_ch` is correct +* Phase rotates with frequency, run electrical delay calibration (see [notebook 07](./07_Advanced_Generators_And_Readouts.ipynb)) + +See Also +======== + +- :doc:`../quick_start` - Quick installation guide +- :doc:`../topics/asmv2_cheatsheet` - tProc v2 cheatsheet +- :doc:`../topics/timing` - More details on timeline management diff --git a/docs/source/tutorials/tprocv1/00_Send_receive_pulse.ipynb b/docs/source/tutorials/tprocv1/00_Send_receive_pulse.ipynb new file mode 120000 index 000000000..c52dcab2c --- /dev/null +++ b/docs/source/tutorials/tprocv1/00_Send_receive_pulse.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/00_Send_receive_pulse.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/01_Phase_coherent_readout.ipynb b/docs/source/tutorials/tprocv1/01_Phase_coherent_readout.ipynb new file mode 120000 index 000000000..636e4eb32 --- /dev/null +++ b/docs/source/tutorials/tprocv1/01_Phase_coherent_readout.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/01_Phase_coherent_readout.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/02_Sweeping_variables.ipynb b/docs/source/tutorials/tprocv1/02_Sweeping_variables.ipynb new file mode 120000 index 000000000..0e70b1ed4 --- /dev/null +++ b/docs/source/tutorials/tprocv1/02_Sweeping_variables.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/02_Sweeping_variables.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/03_Conditional_logic.ipynb b/docs/source/tutorials/tprocv1/03_Conditional_logic.ipynb new file mode 120000 index 000000000..7baf01b21 --- /dev/null +++ b/docs/source/tutorials/tprocv1/03_Conditional_logic.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/03_Conditional_logic.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/04_Reading_Math_Writing.ipynb b/docs/source/tutorials/tprocv1/04_Reading_Math_Writing.ipynb new file mode 120000 index 000000000..e2352e48c --- /dev/null +++ b/docs/source/tutorials/tprocv1/04_Reading_Math_Writing.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/04_Reading_Math_Writing.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/05_PhaseCoherence_QickProgram.ipynb b/docs/source/tutorials/tprocv1/05_PhaseCoherence_QickProgram.ipynb new file mode 120000 index 000000000..655a7ad2f --- /dev/null +++ b/docs/source/tutorials/tprocv1/05_PhaseCoherence_QickProgram.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/05_PhaseCoherence_QickProgram.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/06_qubit_demos.ipynb b/docs/source/tutorials/tprocv1/06_qubit_demos.ipynb new file mode 120000 index 000000000..fb5cb874b --- /dev/null +++ b/docs/source/tutorials/tprocv1/06_qubit_demos.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/06_qubit_demos.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/07_Sweep_ND_variables.ipynb b/docs/source/tutorials/tprocv1/07_Sweep_ND_variables.ipynb new file mode 120000 index 000000000..ddf592d3d --- /dev/null +++ b/docs/source/tutorials/tprocv1/07_Sweep_ND_variables.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/07_Sweep_ND_variables.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/08_Special_buffers.ipynb b/docs/source/tutorials/tprocv1/08_Special_buffers.ipynb new file mode 120000 index 000000000..c1c5c62d4 --- /dev/null +++ b/docs/source/tutorials/tprocv1/08_Special_buffers.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/08_Special_buffers.ipynb \ No newline at end of file diff --git a/docs/source/tutorials/tprocv1/09_photon_counting.ipynb b/docs/source/tutorials/tprocv1/09_photon_counting.ipynb new file mode 120000 index 000000000..36e9fe725 --- /dev/null +++ b/docs/source/tutorials/tprocv1/09_photon_counting.ipynb @@ -0,0 +1 @@ +../../../../qick_demos/09_photon_counting.ipynb \ No newline at end of file diff --git a/docs/topics/asmv1_cheatsheet.rst b/docs/topics/asmv1_cheatsheet.rst deleted file mode 100644 index 252b66875..000000000 --- a/docs/topics/asmv1_cheatsheet.rst +++ /dev/null @@ -1,58 +0,0 @@ -tProc v1 ASM cheatsheet -======================= - -.. list-table:: - :widths: 50 50 - :header-rows: 1 - - * - Python calls - - - * - ``soc.tproc.single_write(addr=imm, data=variable)`` - - writes the value ``variable`` to the data memory address ``imm``. - -.. list-table:: - :widths: 50 50 - :header-rows: 1 - - * - ASM_Program calls - - - * - ``p.memri(p, r, imm, 'comment')`` - - reads the data memory at the address specified by ``imm`` and writes the result into page ``p``, register ``r``. - * - ``p.regwi(p,r,value)`` - - writes the ``value`` to page ``p``, register ``r``. - * - ``p.bitwi(p, rDst, rSrc, operation, value)`` - - performs the bitwise ``operation`` on the contents of page ``p``, register ``rSrc`` and ``value`` and writes the result in page ``p``, register ``rDst``. ``rSrc`` and ``rDst`` may be the same or different. - * - ``p.bitw(p, rDst, rSrc1, operation, rSrc2`` - - performs the bitwise ``operation`` on two source registers (``rSrc1`` and ``rSrc2``) and puts the result in the destination register ``rDst``, where all three registers are on the same page ``p``. - * - ``p.seti(channel, p, rSrc, time)`` - - takes the value at page ``p``, register ``rSrc`` and sends it to ``channel`` that the specified ``time``. - * - ``p.label(labelName)`` - - marks this location in the program, for use by the ``loopz`` and ``condj`` commands. - * - ``p.set(channel, p, ra, rb,rc,rd,re,time)`` - - sends the values on page ``p`` registers ``ra``, ``rb``, ``rc``, ``rd``, ``re`` to ``channel`` at ``time``. The registers ``ra`` through ``re`` contain, in order, 16-bit values of frequency, phase, address, gain, and ( ``nsamp`` , ``outsel`` , ``mode`` , ``stdysel`` ). - * - ``p.sync(p,r)`` - - synchronizes the internal time offset to the value specified by page ``p``,register ``r``. - * - ``p.synci(p,timeOffset)`` - - synchronizes the internal time offset by ``timeOffset``. - -.. list-table:: - :widths: 50 50 - :header-rows: 1 - - * - QickProgram bitwise operations - - - * - ``<<`` - - shifts bits left by ``value`` bits, ignores ``rSrc`` - * - ``|`` - - or - * - ``&`` - - and - * - ``^`` - - exclusive or - * - ``~`` - - not ``value``, ignores ``rSrc`` - -tProcessor register information -------------------------------- - -The tproc contains 8 pages of 32 registers each, making 256 registers in total. Each register is 32 bits wide. diff --git a/docs/topics/freq_matching.rst b/docs/topics/freq_matching.rst deleted file mode 100644 index f32b31c42..000000000 --- a/docs/topics/freq_matching.rst +++ /dev/null @@ -1,20 +0,0 @@ -How to ensure frequency matching -================================ - -Generators and readouts have different frequency units, and the frequencies used in the two systems must be exactly equal. -If they are not, there will be a small difference between the upconversion and downconversion frequencies - this will manifest as a sliding phase, and so you will not see a consistent phase between acquisitions. - -There are two ways to ensure frequency matching: - -* When converting a frequency to an integer value (often with :meth:`.QickConfig.freq2reg()` or :meth:`.AbsQickProgram.declare_readout()`), specify not only the channel you are configuring, but the channel you want to be frequency-matched to. -* Before doing any conversion, round the frequency to the closest frequency that is valid on both channels using :meth:`.QickConfig.adcfreq()`. - -Frequency-matching makes your frequency resolution worse, since the smallest possible frequency step is now the LCM of the two channels' frequency steps. -Usually this doesn't matter - O(10 Hz) resolution is ample for most applications - but you can disable frequency-matching by specifying None as the other channel. - -You may have a generator that does not itself drive any readouts, but needs to be phase-locked to a generator that does. -In this case you will want to frequency-match both generators to that readout, otherwise the two generators will have slightly different frequencies and you will have a sliding phase between them. - -If you have multiple generators (or readouts) of the same type and sampling frequency, it doesn't matter which you use to specify the matched channel. -In many QICK firmwares, all generator and readout channels are the same and it's OK to just use ch 0 for the matched channel. -But it's a good habit to do this consistently correctly. diff --git a/docs/topics/index.rst b/docs/topics/index.rst deleted file mode 100644 index 068111449..000000000 --- a/docs/topics/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Specific topics -=============== - -.. toctree:: - :maxdepth: 1 - - asmv1_cheatsheet - timing - freq_matching - playing_pulses - gen_config - units - changing_fs - reference_clock diff --git a/docs/topics/playing_pulses.rst b/docs/topics/playing_pulses.rst deleted file mode 100644 index 4e9ca76e9..000000000 --- a/docs/topics/playing_pulses.rst +++ /dev/null @@ -1,41 +0,0 @@ -How to play pulses -================== - -There are three steps to playing a pulse: - -Loading a waveform ------------------- - -(You skip this step for rectangular "const" pulses, which have no envelope.) - -Each signal generator has an internal waveform memory, which stores the I/Q data for the pulse envelope. -Multiple waveforms can be stored in the same signal generator, and a single waveform can be used for different pulses (e.g. a Gaussian waveform can be used for Gaussian pulses and the ramp-up/ramp-down of flat-top pulses with different flat-top duration, each with its own gain and carrier frequency). - -:meth:`.AbsQickProgram.add_envelope()` writes an arbitrary waveform to the specified channel's waveform memory. -:meth:`.AbsQickProgram.add_gauss()`, :meth:`.AbsQickProgram.add_triangle()`, and :meth:`.AbsQickProgram.add_DRAG()` write commonly-used standard pulse waveforms, with duration units of fabric clock cycles. The name is used in the next step. - -Setting registers ------------------ - -There are a lot of parameters that need to be specified when playing a pulse - more than can be specified inline in a tProcessor instruction. -So all the parameters must be written to registers first, and when we fire the pulse we just tell the tProcessor which registers to read. - -:meth:`.QickProgram.set_pulse_registers()` writes the settings for a pulse to registers. -All arguments to this method must be integers in the native units of the signal generator. -This can happen immediately before you fire the pulse, but if a signal generator is only used for one type of pulse you will save time for the tProcessor by setting registers in initialization, before the program loop. -If you have some parameters that change from pulse to pulse, and some that never change, you can write the fixed parameters in initialization using :meth:`.QickProgram.default_pulse_registers()` and the varying parameters in the body with :meth:`.QickProgram.set_pulse_registers()`. - -If you want to modify pulse parameters on the fly (for example, you might want to sweep the frequency of the qubit drive pulse), you will set the registers in two steps (you can see an example in the demo :repofile:`qick_demos/02_Sweeping_variables.ipynb` ): - -First, use :meth:`.QickProgram.set_pulse_registers()` to write initial values for all parameters. You could do this in initialization, or right before the following step. -Second, overwrite the register(s) you want to update. -You need to get the page and address of the register using :meth:`.QickProgram.ch_page()` and :meth:`.QickProgram.sreg()`. -Then you can use assembly instructions to change the value of that register. - -Firing the pulse ----------------- - -:meth:`.QickProgram.pulse()` fires a pulse on the specified channel at the specified time, using whatever values are loaded in the registers. - -Often you will want to trigger the readout at the same time: :meth:`.QickProgram.measure()` is a wrapper around :meth:`.QickProgram.trigger()` and :meth:`.QickProgram.pulse()`. -But it's good to remember and understand the building blocks inside this wrapper. diff --git a/docs/topics/timing.rst b/docs/topics/timing.rst deleted file mode 100644 index 686079323..000000000 --- a/docs/topics/timing.rst +++ /dev/null @@ -1,62 +0,0 @@ -Timing -====== - -* Every generator contains a timed FIFO queue. - There is only one master clock time `t_master`, which is shared by all timed queues. - - * Treat :meth:`.QickProgram.trigger()` as a generator channel, i.e. equivalent to :meth:`.QickProgram.pulse()` in terms of clocks. - -* The tproc executes commands as soon as possible, meaning it has no concept of the master time (aside from wait commands, explained later). - It only cares about what point it is at in your code. - There is a master time offset `t_off`, which is incremented by sync commands. - When executed by the tproc, ``trigger(t)`` and ``pulse(t)`` commands are appended to the appropriate queue with a timestamp of ``t_off + t``. - -* A generator plays pulses in the order received. - It waits to play each pulse until the previous pulse is complete and the master clock time equals (or exceeds) the command timestamp. - -* ``pulse(ch,t)`` will play the pulse at ``t`` relative to the clock as defined by the sync command that has been most recently executed by the tproc, UNLESS it is delayed by a wait command (the generator can't play a pulse before the tproc executes the command) or another pulse command (the generator can't cut one pulse short to play the next one). - In that case, it will play at time ``t`` or at the end of the wait time/previous pulse, whatever is latest. - - * To make life easier, you can just always call ``pulse(ch, t=0)``, which will just play the pulse at the next earliest possible time. Then insert simple waits between pulses using ``sync_all(t=wait_time)``. - -* wait commands pause the tproc (i.e. pauses execution of commands). - They do not affect the master time. - Any commands lower down in your code, even if scheduled for some time (defined by the master time) during the wait period, will execute, at the earliest, immediately after the end of the wait period. - - * wait commands are necessary to use with readout/data acquisition timing management, as the tproc does not know when pulses/readout finish and you need to force it to wait for readout to finish. - In other words, you should have a wait at the end of a loop (or the tproc will tell the software to read data from the buffer too soon), or before a ``read`` command (e.g. for feedback). - `This is the only reason to use wait commands.` - - * ``waiti(t)`` is the generic form of wait, which pauses the tproc until the master clock time equals ``t_off + t``. Note ``waiti`` has a channel argument, but the channel argument does nothing! - - * :meth:`.QickProgram.wait_all()` calculates the end of all readout pulses and waits until that time + ``t``. - -* sync commands increment the master time offset (used by all gen channels). - The effect is to push back the play time of subsequent commands by that increment. - - * ``synci(t)`` is the generic form of sync. - - * The safer version :meth:`.QickProgram.sync_all()` calculates the end of the last pulse played + ``t``, and sets the master time offset to that value. - -How to avoid timing problems ----------------------------- - -Timed instructions are executed in two steps: first when the control core of the tProcessor pushes the instruction into the timed queue, and second when the instruction pops off the timed queue. - -* The control core executes all instructions in order and (except ``wait``/``waiti``) as quickly as possible. - If you have a non-timed instruction with external effects, it will generally execute before the timed instructions before and after it. - Specifically, programs (like the AveragerProgram framework we're using here) typically use ``memwi`` to update a data counter, and then read that counter to know how many data samples to get from the buffers - if the ``memwi`` happens before the readout is triggered or completed, you will read invalid data. - -* If the instruction is pushed later than the specified time, it will execute later than specified - this can lead to variations in the relative timing of pulses and readout windows. You are responsible for keeping the control core running ahead of the instruction times. - -Things you should do in your programs: - -* Your initialization should contain a ``synci`` instruction which gives the tProcessor time to get ahead of the clock. - -* Put a :meth:`.QickProgram.sync_all()` somewhere in your loop body, probably at the end. This ensures that you don't have pulses or readouts that exceed the loop length. - -* Put a ``waiti`` instruction after the last readout in your loop body, to make sure the tProcessor doesn't prematurely update the data counter. - -* Follow the ``waiti`` with a ``synci`` that exceeds the ``waiti``, to allow the tProcessor to get ahead of the clock. - -* The last three points are automatically addressed if the last timed instruction in your loop body is a :meth:`.QickProgram.measure()` instruction that specifies ``wait=True`` and a nonzero ``syncdelay``. diff --git a/docs/topics/units.rst b/docs/topics/units.rst deleted file mode 100644 index 1bff8ed52..000000000 --- a/docs/topics/units.rst +++ /dev/null @@ -1,13 +0,0 @@ -Time units -========== - -Time durations are generally specified in units of clock cycles. -The relevant clocks are the tProcessor clock and the fabric clocks of the generators and readouts. -In general these can all be different (and can even vary among generators or readouts). - -For convenience, the :meth:`.QickConfig.us2cycles()` and :meth:`.QickConfig.cycles2us()` methods will convert between floating-point times and integer cycles. -You should be careful to specify which clock you are using, and set the appropriate parameter in :meth:`.QickConfig.us2cycles()`: - -Pulse parameters (the length parameter to :meth:`.QickProgram.set_pulse_registers()`, the length and sigma parameters to :meth:`.AbsQickProgram.add_gauss()`) use the generator clock and you should specify ``gen_ch``. -Readout parameters (the length parameter to :meth:`.AbsQickProgram.declare_readout()`) use the readout clock and you should specify ``ro_ch``. -All other values will use the tProc clock. This includes ``sync`` and ``wait`` commands, and any sort of delay (``t``, ``adc_trig_offset``). Don't use a channel ID parameter. diff --git a/qick_demos/00_Send_receive_pulse.ipynb b/qick_demos/00_Send_receive_pulse.ipynb index 96f1b5617..3bdbc7545 100644 --- a/qick_demos/00_Send_receive_pulse.ipynb +++ b/qick_demos/00_Send_receive_pulse.ipynb @@ -1,5 +1,14 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 00 - Sending and receiving a pulse\n", + "\n", + "**Objective:** Verify your QICK installation, see signals in loopback, understand the different pulse shapes." + ] + }, { "cell_type": "code", "execution_count": 1, diff --git a/qick_demos/01_Phase_coherent_readout.ipynb b/qick_demos/01_Phase_coherent_readout.ipynb index d43fede95..9d99e37dc 100644 --- a/qick_demos/01_Phase_coherent_readout.ipynb +++ b/qick_demos/01_Phase_coherent_readout.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Calibrating the QICK for phase coherent readout\n", + "# 01 - Phase coherent readout\n", "\n", - "###
In this demo you will calibrate the QICK clocks to have the same phase.\n", + "**Objective:** Understand the properties of QICK's \"phase-coherent\" generators and readouts, and practice calibrating their relative phase delays.", "\n", "Before you measure a resonance with your QICK this is the first calibration you should do. It is a calibration for the two synthesizers which belong to the QICK signal generator and the QICK readout, respectively. The two synthesizers are running at the same frequency, but there is initially a constant phase difference $\\phi$ between these two synthesizers. Doing this calibration results in you finding that phase difference $\\phi$. In your subsequent measurements, you can specify this initial phase difference $\\phi$ to compensate for it. From then on, the signal generator can synthesize any frequency and then if you read in data (doing a digital down conversion in the process), the readout will still be phase coherent with respect to the signal generator. \n", "\n", diff --git a/qick_demos/02_Sweeping_variables.ipynb b/qick_demos/02_Sweeping_variables.ipynb index 4b43203f8..ec015c25d 100644 --- a/qick_demos/02_Sweeping_variables.ipynb +++ b/qick_demos/02_Sweeping_variables.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Sweeping variables demonstration\n", + "# 02 - Sweeping variables\n", "\n", - "###
In this demo you will sweep the amplitude of a pulse in loopback to demonstrate control over the QICK. \n", + "**Objective:** Sweep the amplitude of a pulse in loopback to demonstrate control over the QICK. \n", "\n", "We will sweep in two ways:\n", "* Within the QICK assembly language (advantage: faster, disadvantage: cannot be used to sweep the readout frequency)\n", diff --git a/qick_demos/03_Conditional_logic.ipynb b/qick_demos/03_Conditional_logic.ipynb index 906f01314..ca774b26e 100644 --- a/qick_demos/03_Conditional_logic.ipynb +++ b/qick_demos/03_Conditional_logic.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Conditional logic with the QICK demonstration\n", + "# 03 - Conditional logic\n", "\n", - "In this demo you will send and receive a pulse in loopback conditional on the value of a number written to the QICK. A similar application of the QICK conditional logic can enable the active reset of qubits. " + "**Objective:** Send and receive a pulse in loopback conditional on the value of a number written to the QICK. A similar application of the QICK conditional logic can enable the active reset of qubits. " ] }, { diff --git a/qick_demos/04_Reading_Math_Writing.ipynb b/qick_demos/04_Reading_Math_Writing.ipynb index fc81319c2..6f3ff1c4c 100644 --- a/qick_demos/04_Reading_Math_Writing.ipynb +++ b/qick_demos/04_Reading_Math_Writing.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Reading, math and writing demonstration\n", + "# 04 - Reading, math, and writing\n", "\n", - "###
In this demo you will use the tProc to do some math and/or bitwise operations on two variables that you will pass in through the config dictionary and take out with single_read ." + "**Objective:** Use the tProc to do some math and/or bitwise operations on two variables that you will pass in through the config dictionary and take out with single_read ." ] }, { diff --git a/qick_demos/05_PhaseCoherence_QickProgram.ipynb b/qick_demos/05_PhaseCoherence_QickProgram.ipynb index a03058eca..81667d987 100644 --- a/qick_demos/05_PhaseCoherence_QickProgram.ipynb +++ b/qick_demos/05_PhaseCoherence_QickProgram.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Calibrating the QICK for phase coherent readout\n", + "# 05 - Calibrating the QICK for phase coherent readout (obsolete)\n", "\n", - "###
In this demo you will calibrate the QICK clocks to have the same phase.\n", + "**Objective:** Calibrate the QICK clocks to have the same phase.\n", "\n", "Before you measure a resonance with your QICK this is the first calibration you should do. It is a calibration for the two synthesizers which belong to the QICK signal generator and the QICK readout, respectively. The two synthesizers are running at the same frequency, but there is initially a constant phase difference $\\phi$ between these two synthesizers. Doing this calibration results in you finding that phase difference $\\phi$. In your subsequent measurements, you can specify this initial phase difference $\\phi$ to compensate for it. From then on, the signal generator can synthesize any frequency and then if you read in data (doing a digital down conversion in the process), the readout will still be phase coherent with respect to the signal generator. \n", "\n", diff --git a/qick_demos/06_qubit_demos.ipynb b/qick_demos/06_qubit_demos.ipynb index 37ff75e75..51f0c16bf 100644 --- a/qick_demos/06_qubit_demos.ipynb +++ b/qick_demos/06_qubit_demos.ipynb @@ -4,7 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Qubit demos - working with a real qubit\n", + "# 06 - Qubit demos: working with a real qubit\n", + "\n", + "**Objective:** Practice tuning up a single superconducting qubit.\n", "\n", "The measurements in this notebook were used for the QICK paper https://arxiv.org/abs/2110.00557.\n", " \n", @@ -65,7 +67,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Helper functions" + "## Helper functions" ] }, { @@ -182,7 +184,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Configuring the channels\n", + "## Configuring the channels\n", "\n", "The data in this notebook was taken with a wiring diagram similar to the above. The QICK has eight RF DACs which can be configured in multiple ways. Using the QICK, two RF DACs are needed to control the qubit and its readout resonator, respectively. Also, the qubit control pulses (< 6 GHz) were directly synthesized without the use of an analog mixer. Note that the QICK RF board was not used for these measurements, only the ZCU111 evaluation board plus the analog components specified in the diagram." ] @@ -234,7 +236,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# TOF Measurement\n", + "## TOF Measurement\n", "\n", "Measures the \"time of flight\" of the measurement pulse. The time of flight is the time at which the measurement pulse appears in the ADC buffer. We only want to start capturing data from this point in time onwards. We store this time of flight in the parameter readout_cfg[\"adc_trig_offset\"] ." ] @@ -347,7 +349,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Single tone (Resonator) Spectroscopy\n", + "## Single tone (Resonator) Spectroscopy\n", "\n", "Measures the resonant frequency of the readout resonator when the qubit is in its ground state. We store this resonant frequency in the parameter readout_cfg[\"f_res\"] ." ] @@ -456,7 +458,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Single Shot\n", + "## Single Shot\n", "\n", "Measures the single shot readout fidelity of the system. We acquire single shot (I, Q) readout values by first preparing the qubit in its ground (blue dots) a certain number of times (in the below demo we take 5000 shots) and then preparing the qubit in its excited state (red dots) the same number of times. We then extract two parameters which are used to optimize the associated readout fidelity: the rotation angle of the IQ blobs and the threshold that classifies the two qubit states (ground and excited). We store these two parameters here readout_cfg[\"res_phase\"] and readout_cfg[\"threshold\"] .\n", "\n", @@ -598,7 +600,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Pulse Probe (qubit) spectroscopy\n", + "## Pulse Probe (qubit) spectroscopy\n", "\n", "Measures the qubit frequency f_ge. We store this parameter here: qubit_cfg[\"f_ge\"] .\n", "\n", @@ -718,7 +720,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Length Rabi\n", + "## Length Rabi\n", "\n", "Measures Rabi oscillations by sweeping over the duration of the qubit drive pulse. This is a preliminary measurement to prove that we see Rabi oscillations. This measurement is followed up by the Amplitude Rabi experiment below.\n", "\n", @@ -836,7 +838,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Amplitude Rabi\n", + "## Amplitude Rabi\n", "\n", "Measures Rabi oscillations by sweeping over the amplitude of the qubit drive pulse, which is an $\\approx$ 100 ns ($\\sigma$ $\\approx$ 25 ns) long Gaussian pulse. We measure the $\\pi$ and $\\pi$/2 pulse amplitudes which are used in subsequent experiments to control the qubit. We store these parameters here: qubit_cfg[\"pi_gain\"] and qubit_cfg[\"pi2_gain\"] " ] @@ -955,7 +957,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# T1 \n", + "## T1 \n", "\n", "Measures the qubit T1 characteristic decay time by preparing the qubit in its excited state with a $\\pi$ pulse followed by a variable delay ($\\tau$) before measuring the qubit's state. This exponential decay curve can be fitted in post-processing to obtain the qubit T1 time." ] @@ -1072,7 +1074,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Ramsey Fringe Experiment\n", + "## Ramsey Fringe Experiment\n", "\n", "Measures the qubit T2 Ramsey characteristic decay time by preparing the qubit in a superposition state with a $\\frac{\\pi}{2}$ pulse followed by a variable delay ($\\tau$) before applying another $\\frac{\\pi}{2}$ pulse with a phase advanced by $\\Delta \\phi = \\omega * \\tau$, where $\\omega$ is the Ramsey frequency. This exponential decay envelope can be fitted in post-processing to obtain the qubit T2 Ramsey time." ] @@ -1199,7 +1201,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Active reset\n", + "## Active reset\n", "\n", "This experiment demonstrates active reset with the QICK. \n", "\n", diff --git a/qick_demos/07_Sweep_ND_variables.ipynb b/qick_demos/07_Sweep_ND_variables.ipynb index 7bf1dea53..42442e767 100644 --- a/qick_demos/07_Sweep_ND_variables.ipynb +++ b/qick_demos/07_Sweep_ND_variables.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "#
Sweeping over multiple variables demonstration\n", + "# 07 - Sweeping over multiple variables\n", "\n", - "###
This notebook demonstrates how to run a ND variable sweep using the NDAveragerProgram.\n", + "**Objective:** Run a ND variable sweep using the NDAveragerProgram.\n", "\n", "When running parameter sweep experiments, to cancel the effect of slow drifting of room temperature electronics, we usually prefer to run all the sweeps in the fast FPGA program, and put the repetition as the last iteration axis. \n", "\n", diff --git a/qick_demos/08_Special_buffers.ipynb b/qick_demos/08_Special_buffers.ipynb index 9fbf84da0..d0dc75257 100644 --- a/qick_demos/08_Special_buffers.ipynb +++ b/qick_demos/08_Special_buffers.ipynb @@ -1,5 +1,14 @@ { "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# 08 - Special buffers\n", + "\n", + "**Objective:** Learn how to use the special (DDR4 and MR) readout buffers.." + ] + }, { "cell_type": "code", "execution_count": 1, @@ -156,7 +165,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# setup\n", + "## setup\n", "The board should be cabled in loopback; set the channel numbers and frequency here." ] }, @@ -175,7 +184,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# some notes\n", + "## some notes\n", "Remember that the QICK readout works in two steps: first digital downconversion (multiplying by a complex sinusoid), then decimation (low-pass filter and downsample to reduce the bandwidth and sample rate by a factor of 8).\n", "\n", "In normal readout, the decimated data stream goes to the average+buffer module which implements the \"decimated\" and \"accumulated\" readout modes. But we have additional buffer modules that can look at the data stream: the DDR4 and multi-rate buffers.\n", @@ -227,7 +236,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# example program\n", + "## example program\n", "This is identical to the 00 loopback demo, with the addition of the DDR4 and MR triggers. So the only lines you should look at are at the end of `initialize()`.\n", "\n", "You only need to trigger the special buffers once (later triggers are harmless but ignored).\n", @@ -306,7 +315,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# let's do a decimated readout\n", + "## let's do a decimated readout\n", "Note that if you arm a buffer and then run the program multiple times (such as by using soft_avgs/rounds, as we do here), the buffer will only start the first time.\n" ] }, @@ -408,7 +417,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## looking at DDR4 buffer\n", + "### looking at DDR4 buffer\n", "The DDR4 and decimated buffers capture identical data, but the DDR4 buffer has much more capacity.\n", "You generally won't use the full capcity of the DDR4 buffer; only arm and read what you need." ] @@ -480,7 +489,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## looking at MR buffer\n", + "### looking at MR buffer\n", "The MR buffer always captures and returns the full size of the buffer.\n", "\n", "We're looking directly at the output of downconversion, which is the result of taking the real ADC signal (which is the sum of two complex tones at positive and negative frequencies) and shifting it by the RF frequency.\n", @@ -544,7 +553,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# now let's do an accumulated readout\n", + "## now let's do an accumulated readout\n", "The DDR4 buffer is totally independent of the program execution, so you can use it with acquire() or acquire_decimated() - this is sort of naturally useful for debugging, since you can take your normal experimental program, just enable the DDR4 buffer, and capture many reps' worth of decimated data\n" ] }, @@ -617,7 +626,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# just triggering the buffer\n", + "## just triggering the buffer\n", "This is a minimal program that just triggers the buffer - so we just see noise." ] }, @@ -1001,7 +1010,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# extra: junk data\n", + "## extra: junk data\n", "The readout logic in both buffers hold some stale data - some data at the end of each readout is lost and shows up at the start of the buffer in the next readout. Normally this junk data is dropped, so you don't need to worry about it (you will just notice that you get slightly less data than you might expect).\n", "\n", "If we disable that cleanup logic, we can see the effect by doing one readout with a tone playing and a second readout with the tone off: the data from the second readout should just be noise, but instead we see the tone at the start of each buffer. We can confirm that the \"junk length\" configured in the QICK library is correct.\n", diff --git a/qick_demos/09_photon_counting.ipynb b/qick_demos/09_photon_counting.ipynb index b62e90f3a..ac36c2389 100644 --- a/qick_demos/09_photon_counting.ipynb +++ b/qick_demos/09_photon_counting.ipynb @@ -17,7 +17,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Edge counting\n", + "# 09 - Edge counting\n", + "\n", + "**Objective:** Learn to use QICK readouts in edge-counting mode.", "\n", "This demo shows how you can use QICK in a special \"edge-counting\" mode, where instead of integrating the I/Q values inside a readout window, QICK counts the number of rising edges inside the window. This functionality was developed by the [QICK-DAWG](https://github.com/sandialabs/qick-dawg) team for photon counting, but can also be used for counting RF pulses.\n", "\n", diff --git a/qick_lib/qick/VERSION b/qick_lib/qick/VERSION index 084425cd8..b82de4610 100644 --- a/qick_lib/qick/VERSION +++ b/qick_lib/qick/VERSION @@ -1 +1 @@ -0.2.431 +0.2.430 diff --git a/qick_lib/qick/qick.py b/qick_lib/qick/qick.py index c8117d3f6..15df38c4b 100644 --- a/qick_lib/qick/qick.py +++ b/qick_lib/qick/qick.py @@ -1954,7 +1954,7 @@ def tt_arm(self, blk): Parameters ---------- blk : int - The time tagger block to arm (index in `time_taggers' list). + The time tagger block to arm (index in `time_taggers` list). """ self.time_taggers[blk].disarm() self.time_taggers[blk].arm() @@ -1965,7 +1965,7 @@ def tt_disarm(self, blk): Parameters ---------- blk : int - The time tagger block to arm (index in `time_taggers' list). + The time tagger block to arm (index in `time_taggers` list). """ self.time_taggers[blk].disarm() @@ -1976,7 +1976,7 @@ def tt_readmem(self, blk, mem): Parameters ---------- blk : int - The time tagger block to read (index in `time_taggers' list). + The time tagger block to read (index in `time_taggers` list). mem : str "ARM", "SMP", "TAG0"/"TAG1"/"TAG2"/"TAG3" """ @@ -1988,7 +1988,7 @@ def tt_reset(self, blk): Parameters ---------- blk : int - The time tagger block to reset (index in `time_taggers' list). + The time tagger block to reset (index in `time_taggers` list). """ #self.time_taggers[blk].reset() self.time_taggers[blk].flush_mems() @@ -1999,7 +1999,7 @@ def tt_config(self, blk, threshold, wr_smp=32, filt=False, slope=False, invert=F Parameters ---------- blk : int - The time tagger block to flush (index in `time_taggers' list). + The time tagger block to flush (index in `time_taggers` list). threshold : int Tag threshold (-2^15 through 2^15-1). wr_smp : int diff --git a/qick_lib/qick/tprocv2_assembler.py b/qick_lib/qick/tprocv2_assembler.py index 1bc30cd85..5bc2a6edf 100644 --- a/qick_lib/qick/tprocv2_assembler.py +++ b/qick_lib/qick/tprocv2_assembler.py @@ -24,6 +24,7 @@ /////////////////////////////////////////////////////////////////////////////// Release (March 2024) +/////////////////////////////////////////////////////////////////////////////// To be used with qick_processor version 17th or Higher.... """