See also

A Jupyter notebook version of this tutorial can be downloaded here.

Sharing Q1 Registers and Immediates#

This tutorial demonstrates the LINQ-based feedback communication path, which enables low-latency communication of registers and immediates between sequencers.

Every message is tagged with a non-zero 8-bit id that determines routing:

ID range

Routing mode

Configuration required

1–15

Self-cast (originating sequencer only)

No

16–255

Intra-cast (within a module) or Multi-cast (across modules)

Yes — before arming

The three Q1ASM commands used in this tutorial are:

Command

Core

Description

fb_com_data <id>, <value>, <t>

Real-time

Send <value> tagged with <id>; <t> is a timing parameter in ns

fb_pop_data <id>, <dest>

Q1

Pop the next message matching <id> into register <dest>

fb_pull_data <id_dest>, <dest>

Q1

Pop the top FIFO entry; store its ID in <id_dest> and value in <dest>

See the LINQ User Guide for full details on routing, latencies, and data types.

Hardware Requirements#

  • A Qblox Cluster with one module containing at least two sequencers.

Setup#

[1]:
from __future__ import annotations

from qcodes.instrument import find_or_create_instrument

from qblox_instruments import Cluster, ClusterType

Scan For Clusters#

We scan for the available devices connected via ethernet using the Plug & Play functionality of the Qblox Instruments package (see Plug & Play for more info).

!qblox-pnp list

[2]:
cluster_ip = "10.10.200.42"
cluster_name = "cluster0"

Connect to Cluster#

We now make a connection with the Cluster.

[3]:
cluster: Cluster = find_or_create_instrument(
    Cluster,
    recreate=True,
    name=cluster_name,
    identifier=cluster_ip,
    dummy_cfg=(
        {
            2: ClusterType.CLUSTER_QCM,
            4: ClusterType.CLUSTER_QRM,
            6: ClusterType.CLUSTER_QCM_RF,
            8: ClusterType.CLUSTER_QRM_RF,
            10: ClusterType.CLUSTER_QTM,
            12: ClusterType.CLUSTER_QRC,
            16: ClusterType.CLUSTER_QSM,
        }
        if cluster_ip is None
        else None
    ),
)

cluster.reset()
print(cluster.get_system_status())
Status: OKAY, Flags: NONE, Slot flags: NONE

Get connected modules#

[4]:
# QRM baseband modules
modules = cluster.get_connected_modules(lambda mod: mod.is_qrm_type and not mod.is_rf_type)
# This uses the module of the correct type with the lowest slot index
module = list(modules.values())[0]
[5]:
module.disconnect_outputs()
module.disconnect_inputs()
module.sequencer0.nco_prop_delay_comp_en(True)

module.sequencer0.mod_en_awg(True)
module.sequencer0.demod_en_acq(True)

# module.sequencer0.connect_sequencer("io0_1")
module.sequencer0.connect_out0("I")
module.sequencer0.connect_out1("Q")
module.sequencer0.connect_acq_I("in0")
module.sequencer0.connect_acq_Q("in1")
[6]:
# Define sequencers to target: (slot_index, sequencer_index)
seq_targets = [(module.slot_idx, 0), (module.slot_idx, 1)]

Intra-cast Using fb_pop_data#

IDs 16–255 must be routed explicitly before the sequencers are armed. With intra-cast, data is delivered to sequencers within the same module.

Use fb_pop_data when the receiver knows the expected message ID in advance, it pops the first message in the FIFO that matches the given ID.

Experiment parameters#

[7]:
INTRA_ID = 16
VALUE = 420

Configure routing#

[8]:
cluster.clear_router()
module.set_local_route(INTRA_ID)  # Deliver ID 16 to all sequencers in this module

Configure sequencers#

[9]:
module.sequencer0.sync_en(True)
module.sequencer1.sync_en(True)

Define Q1ASM programs#

Sequencer 0 (sender): intra-casts the value 420 with ID 16.

Sequencer 1 (receiver): waits for the intra-cast latency (150 ns), then pops the message with ID 16 into R0.

[10]:
prog_sender = f"""
wait_sync       4                           # Synchronize sequencers
fb_com_data     {INTRA_ID}, {VALUE}, 4      # Intra-cast {VALUE} with ID {INTRA_ID}
stop
"""

prog_receiver_pop = f"""
wait_sync       4                           # Synchronize sequencers
wait            160                         # Wait for intra-cast latency (160 ns)
fb_pop_data     {INTRA_ID}, R0              # Pop message with ID {INTRA_ID} into R0
stop
"""

Upload and run#

[11]:
# Upload.
cluster.update_sequences(
    sequences={
        seq_targets[0]: {
            "waveforms": {},
            "program": prog_sender,
            "acquisitions": {},
            "weights": {},
        },
        seq_targets[1]: {
            "waveforms": {},
            "program": prog_receiver_pop,
            "acquisitions": {},
            "weights": {},
        },
    }
)
[12]:
# Arm and start.
cluster.arm_sequencers(sequencers=seq_targets)
cluster.start_sequencers(sequencers=seq_targets)

print(cluster.get_sequencer_statuses(sequencers=seq_targets))
[SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[], warn_flags=[], err_flags=[], log=[]), SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[], warn_flags=[], err_flags=[], log=[])]

Verify result#

[13]:
received_value = module.sequencer1.get_register("R0")

print(f"Sent    : {VALUE}")
print(f"Received: {received_value}")
print("✓ PASS" if received_value == VALUE else f"✗ FAIL — expected {VALUE}, got {received_value}")
Sent    : 420
Received: 420
✓ PASS

Intra-cast using fb_pull_data#

fb_pull_data pops the top entry from the FIFO regardless of ID, and writes both the ID and the value into separate registers. Use it when the receiver cannot predict which message will arrive first.

This section reuses the routing configuration and sender program defined above. Sequencer 0 again intra-casts 420 with ID 16; sequencer 1 receives it via fb_pull_data.

Define Q1ASM program#

[14]:
prog_receiver_pull = """
wait_sync       4                           # Synchronize sequencers
wait            160                         # Wait for intra-cast latency (160 ns)
fb_pull_data    R1, R0                      # Pull message: ID -> R1, value -> R0
stop
"""

Upload and run#

[15]:
# Upload.
cluster.update_sequences(
    sequences={
        seq_targets[0]: {
            "waveforms": {},
            "program": prog_sender,
            "acquisitions": {},
            "weights": {},
        },
        seq_targets[1]: {
            "waveforms": {},
            "program": prog_receiver_pull,
            "acquisitions": {},
            "weights": {},
        },
    }
)
[16]:
# Arm and start.
cluster.arm_sequencers(sequencers=seq_targets)
cluster.start_sequencers(sequencers=seq_targets)

print(cluster.get_sequencer_statuses(sequencers=seq_targets))
[SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[], warn_flags=[], err_flags=[], log=[]), SequencerStatus(status=<SequencerStatuses.OKAY>, state=<SequencerStates.STOPPED>, exit_code=0, info_flags=[], warn_flags=[], err_flags=[], log=[])]

Verify result#

[17]:
received_id = module.sequencer1.get_register("R1")
received_value = module.sequencer1.get_register("R0")

print(f"ID    — sent: {INTRA_ID}, received: {received_id}")
print(f"Value — sent: {VALUE},  received: {received_value}")
print(
    "✓ PASS"
    if received_value == VALUE and received_id == INTRA_ID
    else "✗ FAIL — check routing configuration and FIFO state"
)
ID    — sent: 16, received: 16
Value — sent: 420,  received: 420
✓ PASS

Stop#

Finally, let’s stop the sequencers if they haven’t already and close the instrument connection. One can also display a detailed snapshot containing the instrument parameters before closing the connection by uncommenting the corresponding lines.

[18]:
# Stop all sequencers.
module.stop_sequencer()

# Print status of sequencers 0 and 1 (should now say it is stopped).
print(module.get_sequencer_status(0))
print(module.get_sequencer_status(1))
print()
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []
Status: OKAY, State: STOPPED, Exit Code: 0, Info Flags: FORCED_STOP, Warning Flags: NONE, Error Flags: NONE, Log: []