forked from M-Labs/artiq
pdq: move to https://github.com/m-labs/pdq
This commit is contained in:
parent
5c66b63768
commit
471605ec1e
|
@ -48,7 +48,9 @@ Release notes
|
||||||
* Experiments scheduled with the "flush pipeline" option now proceed when there
|
* Experiments scheduled with the "flush pipeline" option now proceed when there
|
||||||
are lower-priority experiments in the pipeline. Only experiments at the current
|
are lower-priority experiments in the pipeline. Only experiments at the current
|
||||||
(or higher) priority level are flushed.
|
(or higher) priority level are flushed.
|
||||||
* "pdq2" has been renamed "pdq" (in controller and module names).
|
* The PDQ(2/3) driver has been removed and is now being maintained out-of tree
|
||||||
|
at https://github.com/m-labs/pdq. All SPI/USB driver layers, Mediator,
|
||||||
|
CompoundPDQ and examples/documentation has been moved.
|
||||||
|
|
||||||
|
|
||||||
2.4
|
2.4
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
from artiq.language.core import kernel, portable, delay_mu
|
|
||||||
from artiq.coredevice import spi
|
|
||||||
from artiq.devices.pdq.protocol import PDQBase, PDQ_CMD
|
|
||||||
|
|
||||||
|
|
||||||
_PDQ_SPI_CONFIG = (
|
|
||||||
0*spi.SPI_OFFLINE | 0*spi.SPI_CS_POLARITY |
|
|
||||||
0*spi.SPI_CLK_POLARITY | 0*spi.SPI_CLK_PHASE |
|
|
||||||
0*spi.SPI_LSB_FIRST | 0*spi.SPI_HALF_DUPLEX
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class PDQ(PDQBase):
|
|
||||||
"""PDQ smart arbitrary waveform generator stack.
|
|
||||||
|
|
||||||
Provides access to a stack of PDQ boards connected via SPI using PDQ
|
|
||||||
gateware version 3 or later.
|
|
||||||
|
|
||||||
The SPI bus is wired with ``CS_N`` from the core device connected to
|
|
||||||
``F2 IN`` on the master PDQ, ``CLK`` connected to ``F3 IN``, ``MOSI``
|
|
||||||
connected to ``F4 IN`` and ``MISO`` (optionally) connected to ``F5 OUT``.
|
|
||||||
``F1 TTL Input Trigger`` remains as waveform trigger input.
|
|
||||||
Due to hardware constraints, there can only be one board connected to the
|
|
||||||
core device's MISO line and therefore there can only be SPI readback
|
|
||||||
from one board at any time.
|
|
||||||
|
|
||||||
:param spi_device: Name of the SPI bus this device is on.
|
|
||||||
:param chip_select: Value to drive on the chip select lines of the SPI bus
|
|
||||||
during transactions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
kernel_invariants = {"core", "chip_select", "bus"}
|
|
||||||
|
|
||||||
def __init__(self, dmgr, spi_device, chip_select=1, **kwargs):
|
|
||||||
self.core = dmgr.get("core")
|
|
||||||
self.bus = dmgr.get(spi_device)
|
|
||||||
self.chip_select = chip_select
|
|
||||||
PDQBase.__init__(self, **kwargs)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def setup_bus(self, write_div=24, read_div=64):
|
|
||||||
"""Configure the SPI bus and the SPI transaction parameters
|
|
||||||
for this device. This method has to be called before any other method
|
|
||||||
if the bus has been used to access a different device in the meantime.
|
|
||||||
|
|
||||||
This method advances the timeline by the duration of two
|
|
||||||
RTIO-to-Wishbone bus transactions.
|
|
||||||
|
|
||||||
:param write_div: Write clock divider.
|
|
||||||
:param read_div: Read clock divider.
|
|
||||||
"""
|
|
||||||
# write: 4*8ns >= 20ns = 2*clk (clock de-glitching 50MHz)
|
|
||||||
# read: 15*8*ns >= ~100ns = 5*clk (clk de-glitching latency + miso
|
|
||||||
# latency)
|
|
||||||
self.bus.set_config_mu(_PDQ_SPI_CONFIG, write_div, read_div)
|
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def set_reg(self, adr, data, board):
|
|
||||||
"""Set a PDQ register.
|
|
||||||
|
|
||||||
:param adr: Address of the register (``_PDQ_ADR_CONFIG``,
|
|
||||||
``_PDQ_ADR_FRAME``, ``_PDQ_ADR_CRC``).
|
|
||||||
:param data: Register data (8 bit).
|
|
||||||
:param board: Board to access, ``0xf`` to write to all boards.
|
|
||||||
"""
|
|
||||||
self.bus.write((PDQ_CMD(board, 0, adr, 1) << 24) | (data << 16))
|
|
||||||
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def get_reg(self, adr, board):
|
|
||||||
"""Get a PDQ register.
|
|
||||||
|
|
||||||
:param adr: Address of the register (``_PDQ_ADR_CONFIG``,
|
|
||||||
``_PDQ_ADR_FRAME``, ``_PDQ_ADR_CRC``).
|
|
||||||
:param board: Board to access, ``0xf`` to write to all boards.
|
|
||||||
|
|
||||||
:return: Register data (8 bit).
|
|
||||||
"""
|
|
||||||
self.bus.set_xfer(self.chip_select, 16, 8)
|
|
||||||
self.bus.write(PDQ_CMD(board, 0, adr, 0) << 24)
|
|
||||||
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
|
||||||
self.bus.read_async()
|
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
|
||||||
return int(self.bus.input_async() & 0xff) # FIXME: m-labs/artiq#713
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def write_mem(self, mem, adr, data, board=0xf): # FIXME: m-labs/artiq#714
|
|
||||||
"""Write to DAC channel waveform data memory.
|
|
||||||
|
|
||||||
:param mem: DAC channel memory to access (0 to 2).
|
|
||||||
:param adr: Start address.
|
|
||||||
:param data: Memory data. List of 16 bit integers.
|
|
||||||
:param board: Board to access (0-15) with ``0xf = 15`` being broadcast
|
|
||||||
to all boards.
|
|
||||||
"""
|
|
||||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
|
||||||
self.bus.write((PDQ_CMD(board, 1, mem, 1) << 24) |
|
|
||||||
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
|
||||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
|
||||||
for i in data:
|
|
||||||
self.bus.write(i << 16)
|
|
||||||
delay_mu(-self.bus.write_period_mu)
|
|
||||||
delay_mu(self.bus.write_period_mu + self.bus.ref_period_mu)
|
|
||||||
# get to 20ns min cs high
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def read_mem(self, mem, adr, data, board=0xf, buffer=8):
|
|
||||||
"""Read from DAC channel waveform data memory.
|
|
||||||
|
|
||||||
:param mem: DAC channel memory to access (0 to 2).
|
|
||||||
:param adr: Start address.
|
|
||||||
:param data: Memory data. List of 16 bit integers.
|
|
||||||
:param board: Board to access (0-15) with ``0xf = 15`` being broadcast
|
|
||||||
to all boards.
|
|
||||||
"""
|
|
||||||
n = len(data)
|
|
||||||
if not n:
|
|
||||||
return
|
|
||||||
self.bus.set_xfer(self.chip_select, 24, 8)
|
|
||||||
self.bus.write((PDQ_CMD(board, 1, mem, 0) << 24) |
|
|
||||||
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
|
||||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
|
||||||
self.bus.set_xfer(self.chip_select, 0, 16)
|
|
||||||
for i in range(n):
|
|
||||||
self.bus.write(0)
|
|
||||||
delay_mu(-self.bus.write_period_mu)
|
|
||||||
if i > 0:
|
|
||||||
delay_mu(-3*self.bus.ref_period_mu)
|
|
||||||
self.bus.read_async()
|
|
||||||
if i > buffer:
|
|
||||||
data[i - 1 - buffer] = self.bus.input_async() & 0xffff
|
|
||||||
delay_mu(self.bus.write_period_mu)
|
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
|
||||||
self.bus.read_async()
|
|
||||||
for i in range(max(0, n - buffer - 1), n):
|
|
||||||
data[i] = self.bus.input_async() & 0xffff
|
|
|
@ -1 +0,0 @@
|
||||||
from artiq.devices.pdq.mediator import *
|
|
|
@ -1,35 +0,0 @@
|
||||||
class CRC:
|
|
||||||
"""Generic and simple table driven CRC calculator.
|
|
||||||
|
|
||||||
This implementation is:
|
|
||||||
|
|
||||||
* MSB first data
|
|
||||||
* "un-reversed" full polynomial (i.e. starts with 0x1)
|
|
||||||
* no initial complement
|
|
||||||
* no final complement
|
|
||||||
|
|
||||||
Handle any variation on those details outside this class.
|
|
||||||
|
|
||||||
>>> r = CRC(0x1814141AB)(b"123456789") # crc-32q
|
|
||||||
>>> assert r == 0x3010BF7F, hex(r)
|
|
||||||
"""
|
|
||||||
def __init__(self, poly, data_width=8):
|
|
||||||
self.poly = poly
|
|
||||||
self.crc_width = poly.bit_length() - 1
|
|
||||||
self.data_width = data_width
|
|
||||||
self._table = [self._one(i << self.crc_width - data_width)
|
|
||||||
for i in range(1 << data_width)]
|
|
||||||
|
|
||||||
def _one(self, i):
|
|
||||||
for j in range(self.data_width):
|
|
||||||
i <<= 1
|
|
||||||
if i & 1 << self.crc_width:
|
|
||||||
i ^= self.poly
|
|
||||||
return i
|
|
||||||
|
|
||||||
def __call__(self, msg, crc=0):
|
|
||||||
for data in msg:
|
|
||||||
p = data ^ crc >> self.crc_width - self.data_width
|
|
||||||
q = crc << self.data_width & (1 << self.crc_width) - 1
|
|
||||||
crc = self._table[p] ^ q
|
|
||||||
return crc
|
|
|
@ -1,105 +0,0 @@
|
||||||
# Copyright 2013-2017 Robert Jordens <jordens@gmail.com>
|
|
||||||
#
|
|
||||||
# This file is part of pdq.
|
|
||||||
#
|
|
||||||
# pdq is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# pdq is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with pdq. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
from math import log, sqrt
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
from artiq.wavesynth.coefficients import discrete_compensate
|
|
||||||
from artiq.language.core import kernel, portable, delay_mu
|
|
||||||
|
|
||||||
from .crc import CRC
|
|
||||||
from .protocol import PDQBase, PDQ_CMD
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
crc8 = CRC(0x107)
|
|
||||||
|
|
||||||
|
|
||||||
class PDQ(PDQBase):
|
|
||||||
def __init__(self, url=None, dev=None, **kwargs):
|
|
||||||
"""Initialize PDQ USB/Parallel device stack.
|
|
||||||
|
|
||||||
.. note:: This device should only be used if the PDQ is intended to be
|
|
||||||
configured using the USB connection and **not** via SPI.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url (str): Pyserial device URL. Can be ``hwgrep://`` style
|
|
||||||
(search for serial number, bus topology, USB VID:PID
|
|
||||||
combination), ``COM15`` for a Windows COM port number,
|
|
||||||
``/dev/ttyUSB0`` for a Linux serial port.
|
|
||||||
dev (file-like): File handle to use as device. If passed, ``url``
|
|
||||||
is ignored.
|
|
||||||
**kwargs: See :class:`PDQBase` .
|
|
||||||
"""
|
|
||||||
if dev is None:
|
|
||||||
dev = serial.serial_for_url(url)
|
|
||||||
self.dev = dev
|
|
||||||
self.crc = 0
|
|
||||||
PDQBase.__init__(self, **kwargs)
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
"""Write data to the PDQ board over USB/parallel.
|
|
||||||
|
|
||||||
SOF/EOF control sequences are appended/prepended to
|
|
||||||
the (escaped) data. The running checksum is updated.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data (bytes): Data to write.
|
|
||||||
"""
|
|
||||||
logger.debug("> %r", data)
|
|
||||||
msg = b"\xa5\x02" + data.replace(b"\xa5", b"\xa5\xa5") + b"\xa5\x03"
|
|
||||||
written = self.dev.write(msg)
|
|
||||||
if isinstance(written, int):
|
|
||||||
assert written == len(msg), (written, len(msg))
|
|
||||||
self.crc = crc8(data, self.crc)
|
|
||||||
|
|
||||||
def set_reg(self, adr, data, board):
|
|
||||||
"""Set a register.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
board (int): Board to write to (0-0xe), 0xf for all boards.
|
|
||||||
adr (int): Register address to write to (0-3).
|
|
||||||
data (int): Data to write (1 byte)
|
|
||||||
"""
|
|
||||||
self.write(struct.pack(
|
|
||||||
"<BB", PDQ_CMD(board, False, adr, True), data))
|
|
||||||
|
|
||||||
def write_mem(self, channel, data, start_addr=0):
|
|
||||||
"""Write to channel memory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
channel (int): Channel index to write to. Assumes every board in
|
|
||||||
the stack has :attr:`num_dacs` DAC outputs.
|
|
||||||
data (bytes): Data to write to memory.
|
|
||||||
start_addr (int): Start address to write data to.
|
|
||||||
"""
|
|
||||||
board, dac = divmod(channel, self.num_dacs)
|
|
||||||
self.write(struct.pack("<BH", PDQ_CMD(board, True, dac, True),
|
|
||||||
start_addr) + data)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Close the USB device handle."""
|
|
||||||
self.dev.close()
|
|
||||||
del self.dev
|
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
"""Flush pending data."""
|
|
||||||
self.dev.flush()
|
|
|
@ -1,222 +0,0 @@
|
||||||
from artiq.language import *
|
|
||||||
|
|
||||||
|
|
||||||
frame_setup = 1.5*us
|
|
||||||
sample_period = 10*ns
|
|
||||||
delay_margin_factor = 1 + 1e-4
|
|
||||||
|
|
||||||
|
|
||||||
class FrameActiveError(Exception):
|
|
||||||
"""Raised when a frame is active and playback of a segment from another
|
|
||||||
frame is attempted."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SegmentSequenceError(Exception):
|
|
||||||
"""Raised when attempting to play back a named segment which is not the
|
|
||||||
next in the sequence."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidatedError(Exception):
|
|
||||||
"""Raised when attemting to use a frame or segment that has been
|
|
||||||
invalidated (due to disarming the PDQ)."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ArmError(Exception):
|
|
||||||
"""Raised when attempting to arm an already armed PDQ, to modify the
|
|
||||||
program of an armed PDQ, or to play a segment on a disarmed PDQ."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class _Segment:
|
|
||||||
def __init__(self, frame, segment_number):
|
|
||||||
self.frame = frame
|
|
||||||
self.segment_number = segment_number
|
|
||||||
|
|
||||||
self.lines = []
|
|
||||||
self.duration = 0*s
|
|
||||||
|
|
||||||
# for @kernel
|
|
||||||
self.core = frame.pdq.core
|
|
||||||
|
|
||||||
def add_line(self, duration, channel_data, dac_divider=1):
|
|
||||||
if self.frame.invalidated:
|
|
||||||
raise InvalidatedError()
|
|
||||||
if self.frame.pdq.armed:
|
|
||||||
raise ArmError()
|
|
||||||
self.lines.append((dac_divider, duration, channel_data))
|
|
||||||
self.duration += duration*sample_period/dac_divider
|
|
||||||
|
|
||||||
def get_duration(self):
|
|
||||||
return self.duration
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def advance(self):
|
|
||||||
if self.frame.invalidated:
|
|
||||||
raise InvalidatedError()
|
|
||||||
if not self.frame.pdq.armed:
|
|
||||||
raise ArmError()
|
|
||||||
# If a frame is currently being played, check that we are next.
|
|
||||||
if (self.frame.pdq.current_frame >= 0 and
|
|
||||||
self.frame.pdq.next_segment != self.segment_number):
|
|
||||||
raise SegmentSequenceError()
|
|
||||||
self.frame.advance()
|
|
||||||
|
|
||||||
|
|
||||||
class _Frame:
|
|
||||||
def __init__(self, pdq, frame_number):
|
|
||||||
self.pdq = pdq
|
|
||||||
self.frame_number = frame_number
|
|
||||||
self.segments = []
|
|
||||||
self.segment_count = 0 # == len(self.segments), used in kernel
|
|
||||||
|
|
||||||
self.invalidated = False
|
|
||||||
|
|
||||||
# for @kernel
|
|
||||||
self.core = self.pdq.core
|
|
||||||
|
|
||||||
def create_segment(self, name=None):
|
|
||||||
if self.invalidated:
|
|
||||||
raise InvalidatedError()
|
|
||||||
if self.pdq.armed:
|
|
||||||
raise ArmError()
|
|
||||||
segment = _Segment(self, self.segment_count)
|
|
||||||
if name is not None:
|
|
||||||
if hasattr(self, name):
|
|
||||||
raise NameError("Segment name already exists")
|
|
||||||
setattr(self, name, segment)
|
|
||||||
self.segments.append(segment)
|
|
||||||
self.segment_count += 1
|
|
||||||
return segment
|
|
||||||
|
|
||||||
def _arm(self):
|
|
||||||
self.segment_delays = [
|
|
||||||
self.core.seconds_to_mu(s.duration*delay_margin_factor)
|
|
||||||
for s in self.segments]
|
|
||||||
|
|
||||||
def _invalidate(self):
|
|
||||||
self.invalidated = True
|
|
||||||
|
|
||||||
def _get_program(self):
|
|
||||||
r = []
|
|
||||||
for segment in self.segments:
|
|
||||||
if segment.duration < 2*trigger_duration:
|
|
||||||
raise ValueError(("Segment too short ({:g} s), trigger might "
|
|
||||||
"spill").format(segment.duration))
|
|
||||||
segment_program = [
|
|
||||||
{
|
|
||||||
"dac_divider": dac_divider,
|
|
||||||
"duration": duration,
|
|
||||||
"channel_data": channel_data,
|
|
||||||
"trigger": False,
|
|
||||||
} for dac_divider, duration, channel_data in segment.lines]
|
|
||||||
segment_program[0]["trigger"] = True
|
|
||||||
r += segment_program
|
|
||||||
return r
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def advance(self):
|
|
||||||
if self.invalidated:
|
|
||||||
raise InvalidatedError()
|
|
||||||
if not self.pdq.armed:
|
|
||||||
raise ArmError()
|
|
||||||
|
|
||||||
call_t = now_mu()
|
|
||||||
trigger_start_t = call_t - self.core.seconds_to_mu(trigger_duration/2)
|
|
||||||
|
|
||||||
if self.pdq.current_frame >= 0:
|
|
||||||
# PDQ is in the middle of a frame. Check it is us.
|
|
||||||
if self.pdq.current_frame != self.frame_number:
|
|
||||||
raise FrameActiveError()
|
|
||||||
else:
|
|
||||||
# PDQ is in the jump table - set the selection signals
|
|
||||||
# to play our first segment.
|
|
||||||
self.pdq.current_frame = self.frame_number
|
|
||||||
self.pdq.next_segment = 0
|
|
||||||
at_mu(trigger_start_t - self.core.seconds_to_mu(frame_setup))
|
|
||||||
self.pdq.set_frame(self.frame_number)
|
|
||||||
|
|
||||||
at_mu(trigger_start_t)
|
|
||||||
self.pdq.trigger.pulse(trigger_duration)
|
|
||||||
|
|
||||||
at_mu(call_t)
|
|
||||||
delay_mu(self.segment_delays[self.pdq.next_segment])
|
|
||||||
self.pdq.next_segment += 1
|
|
||||||
|
|
||||||
# test for end of frame
|
|
||||||
if self.pdq.next_segment == self.segment_count:
|
|
||||||
self.pdq.current_frame = -1
|
|
||||||
self.pdq.next_segment = -1
|
|
||||||
|
|
||||||
|
|
||||||
class CompoundPDQ:
|
|
||||||
def __init__(self, dmgr, pdq_devices, trigger_device,
|
|
||||||
aux_miso=0, aux_dac=0b111, clk2x=0):
|
|
||||||
self.core = dmgr.get("core")
|
|
||||||
self.pdqs = [dmgr.get(d) for d in pdq_devices]
|
|
||||||
self.trigger = dmgr.get(trigger_device)
|
|
||||||
self.aux_miso = aux_miso
|
|
||||||
self.aux_dac = aux_dac
|
|
||||||
self.clk2x = clk2x
|
|
||||||
|
|
||||||
self.frames = []
|
|
||||||
self.current_frame = -1
|
|
||||||
self.next_segment = -1
|
|
||||||
self.armed = False
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def disarm(self):
|
|
||||||
for frame in self.frames:
|
|
||||||
frame._invalidate()
|
|
||||||
self.frames.clear()
|
|
||||||
for dev in self.pdqs:
|
|
||||||
dev.set_config(reset=0, clk2x=self.clk2x, enable=0, trigger=0,
|
|
||||||
aux_miso=self.aux_miso, aux_dac=self.aux_dac, board=0xf)
|
|
||||||
self.armed = False
|
|
||||||
|
|
||||||
def get_program(self):
|
|
||||||
return [f._get_program() for f in self.frames]
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def arm(self):
|
|
||||||
if self.armed:
|
|
||||||
raise ArmError()
|
|
||||||
for frame in self.frames:
|
|
||||||
frame._arm()
|
|
||||||
|
|
||||||
full_program = self.get_program()
|
|
||||||
n = 0
|
|
||||||
for pdq in self.pdqs:
|
|
||||||
dn = pdq.get_num_channels()
|
|
||||||
program = []
|
|
||||||
for full_frame_program in full_program:
|
|
||||||
frame_program = []
|
|
||||||
for full_line in full_frame_program:
|
|
||||||
line = {
|
|
||||||
"dac_divider": full_line["dac_divider"],
|
|
||||||
"duration": full_line["duration"],
|
|
||||||
"channel_data": full_line["channel_data"][n:n + dn],
|
|
||||||
"trigger": full_line["trigger"],
|
|
||||||
}
|
|
||||||
frame_program.append(line)
|
|
||||||
program.append(frame_program)
|
|
||||||
pdq.program(program)
|
|
||||||
n += dn
|
|
||||||
for pdq in self.pdqs:
|
|
||||||
dev.set_config(reset=0, clk2x=self.clk2x, enable=1, trigger=0,
|
|
||||||
aux_miso=self.aux_miso, aux_dac=self.aux_dac, board=0xf)
|
|
||||||
self.armed = True
|
|
||||||
|
|
||||||
def create_frame(self):
|
|
||||||
if self.armed:
|
|
||||||
raise ArmError()
|
|
||||||
r = _Frame(self, len(self.frames))
|
|
||||||
self.frames.append(r)
|
|
||||||
return r
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def set_frame(self, frame):
|
|
||||||
for pdq in self.pdqs:
|
|
||||||
pdq.set_frame(self.frame_number)
|
|
|
@ -1,446 +0,0 @@
|
||||||
from math import log, sqrt
|
|
||||||
import logging
|
|
||||||
import struct
|
|
||||||
|
|
||||||
import serial
|
|
||||||
|
|
||||||
from artiq.wavesynth.coefficients import discrete_compensate
|
|
||||||
from artiq.language.core import portable
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
|
||||||
"""Serialize the lines for a single Segment.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
max_time (int): Maximum duration of a line.
|
|
||||||
max_val (int): Maximum absolute value (scale) of the DAC output.
|
|
||||||
max_out (float): Output voltage at :attr:`max_val`. In Volt.
|
|
||||||
out_scale (float): Steps per Volt.
|
|
||||||
cordic_gain (float): CORDIC amplitude gain.
|
|
||||||
addr (int): Address assigned to this segment.
|
|
||||||
data (bytes): Serialized segment data.
|
|
||||||
"""
|
|
||||||
max_time = 1 << 16 # uint16 timer
|
|
||||||
max_val = 1 << 15 # int16 DAC
|
|
||||||
max_out = 10. # Volt
|
|
||||||
out_scale = max_val/max_out
|
|
||||||
cordic_gain = 1.
|
|
||||||
for i in range(16):
|
|
||||||
cordic_gain *= sqrt(1 + 2**(-2*i))
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.data = b""
|
|
||||||
self.addr = None
|
|
||||||
|
|
||||||
def line(self, typ, duration, data, trigger=False, silence=False,
|
|
||||||
aux=False, shift=0, jump=False, clear=False, wait=False):
|
|
||||||
"""Append a line to this segment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
typ (int): Output module to target with this line.
|
|
||||||
duration (int): Duration of the line in units of
|
|
||||||
``clock_period*2**shift``.
|
|
||||||
data (bytes): Opaque data for the output module.
|
|
||||||
trigger (bool): Wait for trigger assertion before executing
|
|
||||||
this line.
|
|
||||||
silence (bool): Disable DAC clocks for the duration of this line.
|
|
||||||
aux (bool): Assert the AUX (F5 TTL) output during this line.
|
|
||||||
The corresponding global AUX routing setting determines which
|
|
||||||
channels control AUX.
|
|
||||||
shift (int): Duration and spline evolution exponent.
|
|
||||||
jump (bool): Return to the frame address table after this line.
|
|
||||||
clear (bool): Clear the DDS phase accumulator when starting to
|
|
||||||
exectute this line.
|
|
||||||
wait (bool): Wait for trigger assertion before executing the next
|
|
||||||
line.
|
|
||||||
"""
|
|
||||||
assert len(data) % 2 == 0, data
|
|
||||||
assert len(data)//2 <= 14
|
|
||||||
# assert dt*(1 << shift) > 1 + len(data)//2
|
|
||||||
header = (
|
|
||||||
1 + len(data)//2 | (typ << 4) | (trigger << 6) | (silence << 7) |
|
|
||||||
(aux << 8) | (shift << 9) | (jump << 13) | (clear << 14) |
|
|
||||||
(wait << 15)
|
|
||||||
)
|
|
||||||
self.data += struct.pack("<HH", header, duration) + data
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def pack(widths, values):
|
|
||||||
"""Pack spline data.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
widths (list[int]): Widths of values in multiples of 16 bits.
|
|
||||||
values (list[int]): Values to pack.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
data (bytes): Packed data.
|
|
||||||
"""
|
|
||||||
fmt = "<"
|
|
||||||
ud = []
|
|
||||||
for width, value in zip(widths, values):
|
|
||||||
value = int(round(value * (1 << 16*width)))
|
|
||||||
if width == 2:
|
|
||||||
ud.append(value & 0xffff)
|
|
||||||
fmt += "H"
|
|
||||||
value >>= 16
|
|
||||||
width -= 1
|
|
||||||
ud.append(value)
|
|
||||||
fmt += "hi"[width]
|
|
||||||
try:
|
|
||||||
return struct.pack(fmt, *ud)
|
|
||||||
except struct.error as e:
|
|
||||||
logger.error("can not pack %s as %s (%s as %s): %s",
|
|
||||||
values, widths, ud, fmt, e)
|
|
||||||
raise e
|
|
||||||
|
|
||||||
def bias(self, amplitude=[], **kwargs):
|
|
||||||
"""Append a bias line to this segment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
amplitude (list[float]): Amplitude coefficients in in Volts and
|
|
||||||
increasing powers of ``1/(2**shift*clock_period)``.
|
|
||||||
Discrete time compensation will be applied.
|
|
||||||
**kwargs: Passed to :meth:`line`.
|
|
||||||
"""
|
|
||||||
coef = [self.out_scale*a for a in amplitude]
|
|
||||||
discrete_compensate(coef)
|
|
||||||
data = self.pack([0, 1, 2, 2], coef)
|
|
||||||
self.line(typ=0, data=data, **kwargs)
|
|
||||||
|
|
||||||
def dds(self, amplitude=[], phase=[], **kwargs):
|
|
||||||
"""Append a DDS line to this segment.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
amplitude (list[float]): Amplitude coefficients in in Volts and
|
|
||||||
increasing powers of ``1/(2**shift*clock_period)``.
|
|
||||||
Discrete time compensation and CORDIC gain compensation
|
|
||||||
will be applied by this method.
|
|
||||||
phase (list[float]): Phase/frequency/chirp coefficients.
|
|
||||||
``phase[0]`` in ``turns``,
|
|
||||||
``phase[1]`` in ``turns/clock_period``,
|
|
||||||
``phase[2]`` in ``turns/(clock_period**2*2**shift)``.
|
|
||||||
**kwargs: Passed to :meth:`line`.
|
|
||||||
"""
|
|
||||||
scale = self.out_scale/self.cordic_gain
|
|
||||||
coef = [scale*a for a in amplitude]
|
|
||||||
discrete_compensate(coef)
|
|
||||||
if phase:
|
|
||||||
assert len(amplitude) == 4
|
|
||||||
coef += [p*self.max_val*2 for p in phase]
|
|
||||||
data = self.pack([0, 1, 2, 2, 0, 1, 1], coef)
|
|
||||||
self.line(typ=1, data=data, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class Channel:
|
|
||||||
"""PDQ Channel.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
num_frames (int): Number of frames supported.
|
|
||||||
max_data (int): Number of 16 bit data words per channel.
|
|
||||||
segments (list[Segment]): Segments added to this channel.
|
|
||||||
"""
|
|
||||||
def __init__(self, max_data, num_frames):
|
|
||||||
self.max_data = max_data
|
|
||||||
self.num_frames = num_frames
|
|
||||||
self.segments = []
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""Remove all segments."""
|
|
||||||
self.segments.clear()
|
|
||||||
|
|
||||||
def new_segment(self):
|
|
||||||
"""Create and attach a new :class:`Segment` to this channel.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
:class:`Segment`
|
|
||||||
"""
|
|
||||||
segment = Segment()
|
|
||||||
self.segments.append(segment)
|
|
||||||
return segment
|
|
||||||
|
|
||||||
def place(self):
|
|
||||||
"""Place segments contiguously.
|
|
||||||
|
|
||||||
Assign segment start addresses and determine length of data.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
addr (int): Amount of memory in use on this channel.
|
|
||||||
"""
|
|
||||||
addr = self.num_frames
|
|
||||||
for segment in self.segments:
|
|
||||||
segment.addr = addr
|
|
||||||
addr += len(segment.data)//2
|
|
||||||
assert addr <= self.max_data, addr
|
|
||||||
return addr
|
|
||||||
|
|
||||||
def table(self, entry=None):
|
|
||||||
"""Generate the frame address table.
|
|
||||||
|
|
||||||
Unused frame indices are assigned the zero address in the frame address
|
|
||||||
table.
|
|
||||||
This will cause the memory parser to remain in the frame address table
|
|
||||||
until another frame is selected.
|
|
||||||
|
|
||||||
The frame entry segments can be any segments in the channel.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
entry (list[Segment]): List of initial segments for each frame.
|
|
||||||
If not specified, the first :attr:`num_frames` segments are
|
|
||||||
used as frame entry points.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
table (bytes): Frame address table.
|
|
||||||
"""
|
|
||||||
table = [0] * self.num_frames
|
|
||||||
if entry is None:
|
|
||||||
entry = self.segments
|
|
||||||
for i, frame in enumerate(entry):
|
|
||||||
if frame is not None:
|
|
||||||
table[i] = frame.addr
|
|
||||||
return struct.pack("<" + "H"*self.num_frames, *table)
|
|
||||||
|
|
||||||
def serialize(self, entry=None):
|
|
||||||
"""Serialize the memory for this channel.
|
|
||||||
|
|
||||||
Places the segments contiguously in memory after the frame table.
|
|
||||||
Allocates and assigns segment and frame table addresses.
|
|
||||||
Serializes segment data and prepends frame address table.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
entry (list[Segment]): See :meth:`table`.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
data (bytes): Channel memory data.
|
|
||||||
"""
|
|
||||||
self.place()
|
|
||||||
data = b"".join([segment.data for segment in self.segments])
|
|
||||||
return self.table(entry) + data
|
|
||||||
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def PDQ_CMD(board, is_mem, adr, we):
|
|
||||||
"""Pack PDQ command fields into command byte.
|
|
||||||
|
|
||||||
:param board: Board address, 0 to 15, with ``15 = 0xf`` denoting broadcast
|
|
||||||
to all boards connected.
|
|
||||||
:param is_mem: If ``1``, ``adr`` denote the address of the memory to access
|
|
||||||
(0 to 2). Otherwise ``adr`` denotes the register to access.
|
|
||||||
:param adr: Address of the register or memory to access.
|
|
||||||
(``PDQ_ADR_CONFIG``, ``PDQ_ADR_FRAME``, ``PDQ_ADR_CRC``).
|
|
||||||
:param we: If ``1`` then write, otherwise read.
|
|
||||||
"""
|
|
||||||
return (adr << 0) | (is_mem << 2) | (board << 3) | (we << 7)
|
|
||||||
|
|
||||||
|
|
||||||
PDQ_ADR_CONFIG = 0
|
|
||||||
PDQ_ADR_CRC = 1
|
|
||||||
PDQ_ADR_FRAME = 2
|
|
||||||
|
|
||||||
|
|
||||||
class PDQBase:
|
|
||||||
"""
|
|
||||||
PDQ stack.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
checksum (int): Running checksum of data written.
|
|
||||||
num_channels (int): Number of channels in this stack.
|
|
||||||
num_boards (int): Number of boards in this stack.
|
|
||||||
num_dacs (int): Number of DAC outputs per board.
|
|
||||||
num_frames (int): Number of frames supported.
|
|
||||||
channels (list[Channel]): List of :class:`Channel` in this stack.
|
|
||||||
"""
|
|
||||||
freq = 50e6
|
|
||||||
|
|
||||||
_mem_sizes = [None, (20,), (10, 10), (8, 6, 6)] # 10kx16 units
|
|
||||||
|
|
||||||
def __init__(self, num_boards=3, num_dacs=3, num_frames=32):
|
|
||||||
"""Initialize PDQ stack.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
num_boards (int): Number of boards in this stack.
|
|
||||||
num_dacs (int): Number of DAC outputs per board.
|
|
||||||
num_frames (int): Number of frames supported.
|
|
||||||
"""
|
|
||||||
self.checksum = 0
|
|
||||||
self.num_boards = num_boards
|
|
||||||
self.num_dacs = num_dacs
|
|
||||||
self.num_frames = num_frames
|
|
||||||
self.num_channels = self.num_dacs * self.num_boards
|
|
||||||
m = self._mem_sizes[num_dacs]
|
|
||||||
self.channels = [Channel(m[j] << 11, num_frames)
|
|
||||||
for i in range(num_boards)
|
|
||||||
for j in range(num_dacs)]
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_num_boards(self):
|
|
||||||
return self.num_boards
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_num_channels(self):
|
|
||||||
return self.num_channels
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_num_frames(self):
|
|
||||||
return self.num_frames
|
|
||||||
|
|
||||||
def get_freq(self):
|
|
||||||
return self.freq
|
|
||||||
|
|
||||||
def set_freq(self, freq):
|
|
||||||
self.freq = float(freq)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def set_reg(self, adr, data, board):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_reg(self, adr, board):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def write_mem(self, mem, adr, data, board=0xf):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def read_mem(self, mem, adr, data, board=0xf, buffer=8):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def set_config(self, reset=0, clk2x=0, enable=1,
|
|
||||||
trigger=0, aux_miso=0, aux_dac=0b111, board=0xf):
|
|
||||||
"""Set the configuration register.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
reset (bool): Reset the board. Memory is not reset. Self-clearing.
|
|
||||||
clk2x (bool): Enable the clock multiplier (100 MHz instead of 50
|
|
||||||
MHz)
|
|
||||||
enable (bool): Enable the reading and execution of waveform data
|
|
||||||
from memory.
|
|
||||||
trigger (bool): Soft trigger. Logical or with the ``F1 TTL Input``
|
|
||||||
hardware trigger.
|
|
||||||
aux_miso (bool): Drive SPI MISO on the AUX/``F5 OUT`` TTL port of
|
|
||||||
each board. If ``False``/``0``, drive the masked logical OR of
|
|
||||||
the DAC channels' aux data.
|
|
||||||
aux_dac (int): DAC channel mask for AUX/F5. Each bit represents
|
|
||||||
one channel. AUX/F5 is: ``aux_miso ? spi_miso :
|
|
||||||
(aux_dac & Cat(_.aux for _ in channels) != 0)``
|
|
||||||
board (int): Board to write to (0-0xe), 0xf for all boards.
|
|
||||||
"""
|
|
||||||
config = ((reset << 0) | (clk2x << 1) | (enable << 2) |
|
|
||||||
(trigger << 3) | (aux_miso << 4) | (aux_dac << 5))
|
|
||||||
self.set_reg(PDQ_ADR_CONFIG, config, board)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_config(self, board=0xf):
|
|
||||||
"""Read configuration register.
|
|
||||||
|
|
||||||
.. seealso: :meth:`set_config`
|
|
||||||
"""
|
|
||||||
return self.get_reg(PDQ_ADR_CONFIG, board)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def set_crc(self, crc=0, board=0xf):
|
|
||||||
"""Set/reset the checksum register.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
crc (int): Checksum value to write.
|
|
||||||
board (int): Board to write to (0-0xe), 0xf for all boards.
|
|
||||||
"""
|
|
||||||
self.set_reg(PDQ_ADR_CRC, crc, board)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_crc(self, board=0xf):
|
|
||||||
"""Read checksum register.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`set_crc`
|
|
||||||
"""
|
|
||||||
return self.get_reg(PDQ_ADR_CRC, board)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def set_frame(self, frame, board=0xf):
|
|
||||||
"""Set the current frame.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
frame (int): Frame to select.
|
|
||||||
board (int): Board to write to (0-0xe), 0xf for all boards.
|
|
||||||
"""
|
|
||||||
self.set_reg(PDQ_ADR_FRAME, frame, board)
|
|
||||||
|
|
||||||
@portable
|
|
||||||
def get_frame(self, board=0xf):
|
|
||||||
"""Read frame selection register.
|
|
||||||
|
|
||||||
.. seealso:: :meth:`set_frame`
|
|
||||||
"""
|
|
||||||
return self.get_reg(PDQ_ADR_FRAME, board)
|
|
||||||
|
|
||||||
def program_segments(self, segments, data):
|
|
||||||
"""Append the wavesynth lines to the given segments.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
segments (list[Segment]): List of :class:`Segment` to append the
|
|
||||||
lines to.
|
|
||||||
data (list): List of wavesynth lines.
|
|
||||||
"""
|
|
||||||
for i, line in enumerate(data):
|
|
||||||
dac_divider = line.get("dac_divider", 1)
|
|
||||||
shift = int(log(dac_divider, 2))
|
|
||||||
if 2**shift != dac_divider:
|
|
||||||
raise ValueError("only power-of-two dac_dividers supported")
|
|
||||||
duration = line["duration"]
|
|
||||||
trigger = line.get("trigger", False)
|
|
||||||
for segment, data in zip(segments, line["channel_data"]):
|
|
||||||
silence = data.pop("silence", False)
|
|
||||||
if len(data) != 1:
|
|
||||||
raise ValueError("only one target per channel and line "
|
|
||||||
"supported")
|
|
||||||
for target, target_data in data.items():
|
|
||||||
getattr(segment, target)(
|
|
||||||
shift=shift, duration=duration, trigger=trigger,
|
|
||||||
silence=silence, **target_data)
|
|
||||||
|
|
||||||
def program(self, program, channels=None):
|
|
||||||
"""Serialize a wavesynth program and write it to the channels
|
|
||||||
in the stack.
|
|
||||||
|
|
||||||
The :class:`Channel` targeted are cleared and each frame in the
|
|
||||||
wavesynth program is appended to a fresh set of :class:`Segment`
|
|
||||||
of the channels. All segments are allocated, the frame address tale
|
|
||||||
is generated, the channels are serialized and their memories are
|
|
||||||
written.
|
|
||||||
|
|
||||||
Short single-cycle lines are prepended and appended to each frame to
|
|
||||||
allow proper write interlocking and to assure that the memory reader
|
|
||||||
can be reliably parked in the frame address table.
|
|
||||||
The first line of each frame is mandatorily triggered.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
program (list): Wavesynth program to serialize.
|
|
||||||
channels (list[int]): Channel indices to use. If unspecified, all
|
|
||||||
channels are used.
|
|
||||||
"""
|
|
||||||
if channels is None:
|
|
||||||
channels = range(self.num_channels)
|
|
||||||
chs = [self.channels[i] for i in channels]
|
|
||||||
for channel in chs:
|
|
||||||
channel.clear()
|
|
||||||
for frame in program:
|
|
||||||
segments = [c.new_segment() for c in chs]
|
|
||||||
self.program_segments(segments, frame)
|
|
||||||
# append an empty line to stall the memory reader before jumping
|
|
||||||
# through the frame table (`wait` does not prevent reading
|
|
||||||
# the next line)
|
|
||||||
for segment in segments:
|
|
||||||
segment.line(typ=3, data=b"", trigger=True, duration=1, aux=1,
|
|
||||||
jump=True)
|
|
||||||
for channel, ch in zip(channels, chs):
|
|
||||||
self.write_mem(channel, ch.serialize())
|
|
||||||
|
|
||||||
def ping(self):
|
|
||||||
"""Ping method returning True. Required for ARTIQ remote
|
|
||||||
controller."""
|
|
||||||
return True
|
|
|
@ -172,44 +172,6 @@ device_db = {
|
||||||
"arguments": {"bus_channel": 27, "channel": 2}
|
"arguments": {"bus_channel": 27, "channel": 2}
|
||||||
},
|
},
|
||||||
|
|
||||||
"qc_q1_0": {
|
|
||||||
"type": "controller",
|
|
||||||
# ::1 is the IPv6 localhost address. If this controller is running on a remote machine,
|
|
||||||
# replace it with the IP or hostname of the machine. If using the hostname, make sure
|
|
||||||
# that it always resolves to a network-visible IP address (see documentation).
|
|
||||||
"host": "::1",
|
|
||||||
"port": 4000,
|
|
||||||
"command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_0.bin"
|
|
||||||
},
|
|
||||||
"qc_q1_1": {
|
|
||||||
"type": "controller",
|
|
||||||
"host": "::1",
|
|
||||||
"port": 4001,
|
|
||||||
"command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_1.bin"
|
|
||||||
},
|
|
||||||
"qc_q1_2": {
|
|
||||||
"type": "controller",
|
|
||||||
"host": "::1",
|
|
||||||
"port": 4002,
|
|
||||||
"command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_2.bin"
|
|
||||||
},
|
|
||||||
"qc_q1_3": {
|
|
||||||
"type": "controller",
|
|
||||||
"host": "::1",
|
|
||||||
"port": 4003,
|
|
||||||
"command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_3.bin"
|
|
||||||
},
|
|
||||||
"electrodes": {
|
|
||||||
"type": "local",
|
|
||||||
"module": "artiq.devices.pdq",
|
|
||||||
"class": "CompoundPDQ",
|
|
||||||
"arguments": {
|
|
||||||
"pdq2_devices": ["qc_q1_0", "qc_q1_1", "qc_q1_2", "qc_q1_3"],
|
|
||||||
"trigger_device": "ttl2",
|
|
||||||
"frame_devices": ["ttl4", "ttl5", "ttl6"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
"lda": {
|
"lda": {
|
||||||
"type": "controller",
|
"type": "controller",
|
||||||
"best_effort": True,
|
"best_effort": True,
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from artiq.experiment import *
|
|
||||||
from artiq.wavesynth.coefficients import build_segment
|
|
||||||
|
|
||||||
|
|
||||||
class PDQ2Simple(EnvExperiment):
|
|
||||||
"""Set PDQ2 voltages."""
|
|
||||||
def build(self):
|
|
||||||
self.setattr_device("core")
|
|
||||||
self.setattr_device("pmt")
|
|
||||||
self.setattr_device("electrodes")
|
|
||||||
|
|
||||||
# 1 device, 3 board each, 3 dacs each
|
|
||||||
self.u = np.arange(4*3)[None, :, None]*.1
|
|
||||||
|
|
||||||
def setup(self, offset):
|
|
||||||
self.electrodes.disarm()
|
|
||||||
self.load = self.electrodes.create_frame()
|
|
||||||
segment = self.load.create_segment()
|
|
||||||
for line in build_segment([100], self.u + offset):
|
|
||||||
segment.add_line(**line)
|
|
||||||
self.detect = self.electrodes.create_frame()
|
|
||||||
segment = self.detect.create_segment()
|
|
||||||
for line in build_segment([100], -self.u + offset):
|
|
||||||
segment.add_line(**line)
|
|
||||||
self.electrodes.arm()
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def one(self):
|
|
||||||
self.core.break_realtime()
|
|
||||||
delay(1*ms)
|
|
||||||
self.load.advance()
|
|
||||||
delay(1*ms)
|
|
||||||
self.detect.advance()
|
|
||||||
delay(1*ms)
|
|
||||||
self.pmt.gate_rising(100*us)
|
|
||||||
return self.pmt.count()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.core.reset()
|
|
||||||
offsets = np.arange(0, 3)
|
|
||||||
for o in offsets:
|
|
||||||
self.setup(o)
|
|
||||||
self.one()
|
|
|
@ -1,107 +0,0 @@
|
||||||
# Copyright (C) 2014, 2015 Robert Jordens <jordens@gmail.com>
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
from artiq.experiment import *
|
|
||||||
from artiq.wavesynth.coefficients import SplineSource
|
|
||||||
|
|
||||||
|
|
||||||
class Transport(EnvExperiment):
|
|
||||||
"""Transport"""
|
|
||||||
|
|
||||||
def build(self):
|
|
||||||
self.setattr_device("core")
|
|
||||||
self.setattr_device("bd_sw")
|
|
||||||
self.setattr_device("pmt")
|
|
||||||
self.setattr_device("electrodes")
|
|
||||||
|
|
||||||
self.setattr_argument("wait_at_stop", NumberValue(100*us))
|
|
||||||
self.setattr_argument("speed", NumberValue(1/(10*us)))
|
|
||||||
self.setattr_argument("repeats", NumberValue(100, step=1, ndecimals=0))
|
|
||||||
self.setattr_argument("bins", NumberValue(100, step=1, ndecimals=0))
|
|
||||||
|
|
||||||
t = np.linspace(0, 10, 101) # waveform time
|
|
||||||
u = 1 - np.cos(np.pi*t/t[-1])
|
|
||||||
# 4 devices, 3 board each, 3 dacs each
|
|
||||||
u = np.arange(4*3*3)[:, None]*.1 + u
|
|
||||||
self.data = SplineSource(x=t, y=u)
|
|
||||||
|
|
||||||
def calc_waveforms(self, stop):
|
|
||||||
scale = self.speed/(50*MHz)
|
|
||||||
self.electrodes.disarm()
|
|
||||||
self.tf = self.electrodes.create_frame()
|
|
||||||
self.data.extend_segment(self.tf.create_segment("to_stop"),
|
|
||||||
0, stop, scale=scale)
|
|
||||||
# append the reverse transport (from stop to 0)
|
|
||||||
# both durations are the same in this case
|
|
||||||
self.data.extend_segment(self.tf.create_segment("from_stop"),
|
|
||||||
stop, 0, scale=scale)
|
|
||||||
# distributes frames to the sub-devices in CompoundPDQ2
|
|
||||||
# and uploads them
|
|
||||||
self.electrodes.arm()
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def cool(self):
|
|
||||||
self.bd_sw.pulse(1*ms)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def transport(self):
|
|
||||||
# selects transport frame
|
|
||||||
# triggers pdqs to start transport frame segment
|
|
||||||
# plays the transport waveform from 0 to stop
|
|
||||||
# delay()s the core by the duration of the waveform segment
|
|
||||||
self.tf.to_stop.advance()
|
|
||||||
# leaves the ion in the dark at the transport endpoint
|
|
||||||
delay(self.wait_at_stop)
|
|
||||||
# transport back (again: trigger, delay())
|
|
||||||
# segments can only be advance()ed in order
|
|
||||||
# since this is the last segment, pdq will go back to jump table
|
|
||||||
self.tf.from_stop.advance()
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def detect(self):
|
|
||||||
self.bd_sw.on()
|
|
||||||
self.pmt.gate_rising(100*us)
|
|
||||||
return self.pmt.count()
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def one(self):
|
|
||||||
delay(1*ms)
|
|
||||||
self.cool()
|
|
||||||
self.transport()
|
|
||||||
return self.detect()
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def repeat(self):
|
|
||||||
self.core.break_realtime()
|
|
||||||
hist = [0 for _ in range(self.bins)]
|
|
||||||
for i in range(self.repeats):
|
|
||||||
n = self.one()
|
|
||||||
if n >= self.bins:
|
|
||||||
n = self.bins - 1
|
|
||||||
hist[n] += 1
|
|
||||||
self.set_dataset("hist", hist)
|
|
||||||
|
|
||||||
def scan(self, stops):
|
|
||||||
for s in stops:
|
|
||||||
# non-kernel, build frames
|
|
||||||
# could also be rpc'ed from repeat()
|
|
||||||
self.calc_waveforms(s)
|
|
||||||
# kernel part
|
|
||||||
self.repeat()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
self.core.reset()
|
|
||||||
# scan transport endpoint
|
|
||||||
stops = np.linspace(0, 10, 10)
|
|
||||||
self.scan(stops)
|
|
||||||
|
|
||||||
|
|
||||||
# class Benchmark(Transport):
|
|
||||||
# def build(self):
|
|
||||||
# Transport.build(self)
|
|
||||||
# self.calc_waveforms(.3)
|
|
||||||
#
|
|
||||||
# @kernel
|
|
||||||
# def run(self):
|
|
||||||
# self.repeat()
|
|
|
@ -1,59 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
from artiq.devices.pdq.driver import PDQ
|
|
||||||
from artiq.protocols.pc_rpc import simple_server_loop
|
|
||||||
from artiq.tools import *
|
|
||||||
|
|
||||||
|
|
||||||
def get_argparser():
|
|
||||||
parser = argparse.ArgumentParser(description="""PDQ controller.
|
|
||||||
|
|
||||||
Use this controller for PDQ stacks that are connected via USB.""")
|
|
||||||
simple_network_args(parser, 3252)
|
|
||||||
parser.add_argument("-d", "--device", default=None, help="serial port")
|
|
||||||
parser.add_argument("--simulation", action="store_true",
|
|
||||||
help="do not open any device but dump data")
|
|
||||||
parser.add_argument("--dump", default="pdq_dump.bin",
|
|
||||||
help="file to dump simulation data into")
|
|
||||||
parser.add_argument("-r", "--reset", default=False,
|
|
||||||
action="store_true", help="reset device [%(default)s]")
|
|
||||||
parser.add_argument("-b", "--boards", default=3, type=int,
|
|
||||||
help="number of boards [%(default)s]")
|
|
||||||
verbosity_args(parser)
|
|
||||||
return parser
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = get_argparser().parse_args()
|
|
||||||
init_logger(args)
|
|
||||||
port = None
|
|
||||||
|
|
||||||
if not args.simulation and args.device is None:
|
|
||||||
print("You need to specify either --simulation or -d/--device "
|
|
||||||
"argument. Use --help for more information.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if args.simulation:
|
|
||||||
port = open(args.dump, "wb")
|
|
||||||
dev = PDQ(url=args.device, dev=port, num_boards=args.boards)
|
|
||||||
try:
|
|
||||||
if args.reset:
|
|
||||||
dev.write(b"") # flush eop
|
|
||||||
dev.set_config(reset=True)
|
|
||||||
time.sleep(.1)
|
|
||||||
|
|
||||||
dev.set_crc(0)
|
|
||||||
dev.checksum = 0
|
|
||||||
|
|
||||||
simple_server_loop({"pdq": dev}, bind_address_from_args(args),
|
|
||||||
args.port, description="device=" + str(args.device))
|
|
||||||
finally:
|
|
||||||
dev.close()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
|
@ -1,130 +0,0 @@
|
||||||
# Copyright (C) 2014, 2015 Robert Jordens <jordens@gmail.com>
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import os
|
|
||||||
import io
|
|
||||||
|
|
||||||
from artiq.devices.pdq.driver import PDQ
|
|
||||||
from artiq.wavesynth.compute_samples import Synthesizer
|
|
||||||
|
|
||||||
|
|
||||||
pdq_gateware = os.getenv("ARTIQ_PDQ_GATEWARE")
|
|
||||||
|
|
||||||
|
|
||||||
class TestPdq(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.dev = PDQ(dev=io.BytesIO())
|
|
||||||
self.synth = Synthesizer(3, _test_program)
|
|
||||||
|
|
||||||
def test_reset(self):
|
|
||||||
self.dev.set_config(reset=True)
|
|
||||||
buf = self.dev.dev.getvalue()
|
|
||||||
self.assertEqual(buf, b"\xa5\x02\xf8\xe5\xa5\x03")
|
|
||||||
|
|
||||||
def test_program(self):
|
|
||||||
# about 0.14 ms
|
|
||||||
self.dev.program(_test_program)
|
|
||||||
|
|
||||||
def test_cmd_program(self):
|
|
||||||
self.dev.set_config(enable=False)
|
|
||||||
self.dev.program(_test_program)
|
|
||||||
self.dev.set_config(enable=True, trigger=True)
|
|
||||||
return self.dev.dev.getvalue()
|
|
||||||
|
|
||||||
def test_synth(self):
|
|
||||||
s = self.synth
|
|
||||||
s.select(0)
|
|
||||||
y = s.trigger()
|
|
||||||
return list(zip(*y))
|
|
||||||
|
|
||||||
def run_gateware(self):
|
|
||||||
import sys
|
|
||||||
sys.path.append(pdq_gateware)
|
|
||||||
from gateware.pdq import PdqSim
|
|
||||||
from migen import run_simulation
|
|
||||||
|
|
||||||
def ncycles(n):
|
|
||||||
for i in range(n):
|
|
||||||
yield
|
|
||||||
|
|
||||||
buf = self.test_cmd_program()
|
|
||||||
tb = PdqSim()
|
|
||||||
tb.ctrl_pads.trigger.reset = 1
|
|
||||||
run_simulation(tb, [ncycles(len(buf) + 250)])
|
|
||||||
delays = 7, 10, 30
|
|
||||||
y = list(zip(*tb.outputs[len(buf) + 130:]))
|
|
||||||
y = list(zip(*(yi[di:] for yi, di in zip(y, delays))))
|
|
||||||
self.assertGreaterEqual(len(y), 80)
|
|
||||||
self.assertEqual(len(y[0]), 3)
|
|
||||||
return y
|
|
||||||
|
|
||||||
@unittest.skipUnless(pdq_gateware, "no PDQ gateware")
|
|
||||||
def test_run_compare(self):
|
|
||||||
y_ref = self.test_synth()
|
|
||||||
y = self.run_gateware()
|
|
||||||
|
|
||||||
for i, (yi, yi_ref) in enumerate(zip(y, y_ref)):
|
|
||||||
for j, (yij, yij_ref) in enumerate(zip(yi, yi_ref)):
|
|
||||||
yij = yij*20./2**16
|
|
||||||
if yij > 10:
|
|
||||||
yij -= 20
|
|
||||||
self.assertAlmostEqual(yij, yij_ref, 2, "disagreement at "
|
|
||||||
"t={}, c={}".format(i, j))
|
|
||||||
|
|
||||||
@unittest.skipUnless(pdq_gateware, "no PDQ gateware")
|
|
||||||
@unittest.skip("manual/visual test")
|
|
||||||
def test_run_plot(self):
|
|
||||||
from matplotlib import pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
y_ref = self.test_synth()
|
|
||||||
y_ref = np.array(y_ref)
|
|
||||||
y = self.run_gateware()
|
|
||||||
y = np.array(y, dtype=np.uint16).view(np.int16)
|
|
||||||
y = y*20./2**16
|
|
||||||
plt.step(np.arange(len(y)), y)
|
|
||||||
plt.step(np.arange(len(y_ref)), y_ref, "k")
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
|
|
||||||
_test_program = [
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"trigger": True,
|
|
||||||
"duration": 20,
|
|
||||||
"channel_data": [
|
|
||||||
{"bias": {"amplitude": [0, 0, 2e-3]}},
|
|
||||||
{"bias": {"amplitude": [1, 0, -7.5e-3, 7.5e-4]}},
|
|
||||||
{"dds": {
|
|
||||||
"amplitude": [0, 0, 4e-3, 0],
|
|
||||||
"phase": [.25, .025],
|
|
||||||
}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": 40,
|
|
||||||
"channel_data": [
|
|
||||||
{"bias": {"amplitude": [.4, .04, -2e-3]}},
|
|
||||||
{
|
|
||||||
"bias": {"amplitude": [.5]},
|
|
||||||
"silence": True,
|
|
||||||
},
|
|
||||||
{"dds": {
|
|
||||||
"amplitude": [.8, .08, -4e-3, 0],
|
|
||||||
"phase": [.25, .025, .02/40],
|
|
||||||
"clear": True,
|
|
||||||
}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"duration": 20,
|
|
||||||
"channel_data": [
|
|
||||||
{"bias": {"amplitude": [.4, -.04, 2e-3]}},
|
|
||||||
{"bias": {"amplitude": [.5, 0, -7.5e-3, 7.5e-4]}},
|
|
||||||
{"dds": {
|
|
||||||
"amplitude": [.8, -.08, 4e-3, 0],
|
|
||||||
"phase": [-.25],
|
|
||||||
}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
]
|
|
|
@ -68,9 +68,3 @@ These drivers are for the core device and the peripherals closely integrated int
|
||||||
|
|
||||||
.. automodule:: artiq.coredevice.sawg
|
.. automodule:: artiq.coredevice.sawg
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
:mod:`artiq.coredevice.pdq` module
|
|
||||||
-----------------------------------
|
|
||||||
|
|
||||||
.. automodule:: artiq.coredevice.pdq
|
|
||||||
:members:
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ Default network ports
|
||||||
+--------------------------------+--------------+
|
+--------------------------------+--------------+
|
||||||
| Master (control) | 3251 |
|
| Master (control) | 3251 |
|
||||||
+--------------------------------+--------------+
|
+--------------------------------+--------------+
|
||||||
| PDQ2 | 3252 |
|
| PDQ2 (out-of-tree) | 3252 |
|
||||||
+--------------------------------+--------------+
|
+--------------------------------+--------------+
|
||||||
| LDA | 3253 |
|
| LDA | 3253 |
|
||||||
+--------------------------------+--------------+
|
+--------------------------------+--------------+
|
||||||
|
|
|
@ -8,35 +8,6 @@ Core device logging controller
|
||||||
:ref: artiq.frontend.aqctl_corelog.get_argparser
|
:ref: artiq.frontend.aqctl_corelog.get_argparser
|
||||||
:prog: aqctl_corelog
|
:prog: aqctl_corelog
|
||||||
|
|
||||||
PDQ2
|
|
||||||
----
|
|
||||||
|
|
||||||
Protocol
|
|
||||||
++++++++
|
|
||||||
|
|
||||||
.. automodule:: artiq.devices.pdq.protocol
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Driver
|
|
||||||
++++++
|
|
||||||
|
|
||||||
.. automodule:: artiq.devices.pdq.driver
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Mediator
|
|
||||||
++++++++
|
|
||||||
|
|
||||||
.. automodule:: artiq.devices.pdq.mediator
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Controller
|
|
||||||
++++++++++
|
|
||||||
|
|
||||||
.. argparse::
|
|
||||||
:ref: artiq.frontend.aqctl_pdq.get_argparser
|
|
||||||
:prog: aqctl_pdq
|
|
||||||
|
|
||||||
|
|
||||||
Lab Brick Digital Attenuator (LDA)
|
Lab Brick Digital Attenuator (LDA)
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
||||||
|
|
1
setup.py
1
setup.py
|
@ -40,7 +40,6 @@ console_scripts = [
|
||||||
"aqctl_korad_ka3005p = artiq.frontend.aqctl_korad_ka3005p:main",
|
"aqctl_korad_ka3005p = artiq.frontend.aqctl_korad_ka3005p:main",
|
||||||
"aqctl_lda = artiq.frontend.aqctl_lda:main",
|
"aqctl_lda = artiq.frontend.aqctl_lda:main",
|
||||||
"aqctl_novatech409b = artiq.frontend.aqctl_novatech409b:main",
|
"aqctl_novatech409b = artiq.frontend.aqctl_novatech409b:main",
|
||||||
"aqctl_pdq = artiq.frontend.aqctl_pdq:main",
|
|
||||||
"aqctl_thorlabs_tcube = artiq.frontend.aqctl_thorlabs_tcube:main",
|
"aqctl_thorlabs_tcube = artiq.frontend.aqctl_thorlabs_tcube:main",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue