Robert Jördens 2017-07-19 17:35:28 +02:00
parent 5c66b63768
commit 471605ec1e
16 changed files with 4 additions and 1365 deletions

View File

@ -48,7 +48,9 @@ Release notes
* Experiments scheduled with the "flush pipeline" option now proceed when there
are lower-priority experiments in the pipeline. Only experiments at the current
(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

View File

@ -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

View File

@ -1 +0,0 @@
from artiq.devices.pdq.mediator import *

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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

View File

@ -172,44 +172,6 @@ device_db = {
"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": {
"type": "controller",
"best_effort": True,

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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],
}},
],
},
]
]

View File

@ -68,9 +68,3 @@ These drivers are for the core device and the peripherals closely integrated int
.. automodule:: artiq.coredevice.sawg
:members:
:mod:`artiq.coredevice.pdq` module
-----------------------------------
.. automodule:: artiq.coredevice.pdq
:members:

View File

@ -26,7 +26,7 @@ Default network ports
+--------------------------------+--------------+
| Master (control) | 3251 |
+--------------------------------+--------------+
| PDQ2 | 3252 |
| PDQ2 (out-of-tree) | 3252 |
+--------------------------------+--------------+
| LDA | 3253 |
+--------------------------------+--------------+

View File

@ -8,35 +8,6 @@ Core device logging controller
:ref: artiq.frontend.aqctl_corelog.get_argparser
: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)
----------------------------------

View File

@ -40,7 +40,6 @@ console_scripts = [
"aqctl_korad_ka3005p = artiq.frontend.aqctl_korad_ka3005p:main",
"aqctl_lda = artiq.frontend.aqctl_lda:main",
"aqctl_novatech409b = artiq.frontend.aqctl_novatech409b:main",
"aqctl_pdq = artiq.frontend.aqctl_pdq:main",
"aqctl_thorlabs_tcube = artiq.frontend.aqctl_thorlabs_tcube:main",
]