forked from M-Labs/artiq
pdq: unify spi-PDQ and usb-PDQ protocols
This commit is contained in:
parent
6c54c0f834
commit
566ff73dff
|
@ -1,5 +1,6 @@
|
||||||
from artiq.language.core import kernel, portable, delay_mu
|
from artiq.language.core import kernel, portable, delay_mu
|
||||||
from artiq.coredevice import spi
|
from artiq.coredevice import spi
|
||||||
|
from artiq.devices.pdq.protocol import PDQBase, PDQ_CMD
|
||||||
|
|
||||||
|
|
||||||
_PDQ_SPI_CONFIG = (
|
_PDQ_SPI_CONFIG = (
|
||||||
|
@ -9,27 +10,8 @@ _PDQ_SPI_CONFIG = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@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
|
class PDQ(PDQBase):
|
||||||
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 PDQ:
|
|
||||||
"""PDQ smart arbitrary waveform generator stack.
|
"""PDQ smart arbitrary waveform generator stack.
|
||||||
|
|
||||||
Provides access to a stack of PDQ boards connected via SPI using PDQ
|
Provides access to a stack of PDQ boards connected via SPI using PDQ
|
||||||
|
@ -50,10 +32,11 @@ class PDQ:
|
||||||
|
|
||||||
kernel_invariants = {"core", "chip_select", "bus"}
|
kernel_invariants = {"core", "chip_select", "bus"}
|
||||||
|
|
||||||
def __init__(self, dmgr, spi_device, chip_select=1):
|
def __init__(self, dmgr, spi_device, chip_select=1, **kwargs):
|
||||||
self.core = dmgr.get("core")
|
self.core = dmgr.get("core")
|
||||||
self.bus = dmgr.get(spi_device)
|
self.bus = dmgr.get(spi_device)
|
||||||
self.chip_select = chip_select
|
self.chip_select = chip_select
|
||||||
|
PDQBase.__init__(self, **kwargs)
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def setup_bus(self, write_div=24, read_div=64):
|
def setup_bus(self, write_div=24, read_div=64):
|
||||||
|
@ -82,7 +65,7 @@ class PDQ:
|
||||||
:param data: Register data (8 bit).
|
:param data: Register data (8 bit).
|
||||||
:param board: Board to access, ``0xf`` to write to all boards.
|
:param board: Board to access, ``0xf`` to write to all boards.
|
||||||
"""
|
"""
|
||||||
self.bus.write((_PDQ_CMD(board, 0, adr, 1) << 24) | (data << 16))
|
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
|
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
|
@ -96,57 +79,12 @@ class PDQ:
|
||||||
:return: Register data (8 bit).
|
:return: Register data (8 bit).
|
||||||
"""
|
"""
|
||||||
self.bus.set_xfer(self.chip_select, 16, 8)
|
self.bus.set_xfer(self.chip_select, 16, 8)
|
||||||
self.bus.write(_PDQ_CMD(board, 0, adr, 0) << 24)
|
self.bus.write(PDQ_CMD(board, 0, adr, 0) << 24)
|
||||||
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
delay_mu(self.bus.ref_period_mu) # get to 20ns min cs high
|
||||||
self.bus.read_async()
|
self.bus.read_async()
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
self.bus.set_xfer(self.chip_select, 16, 0)
|
||||||
return int(self.bus.input_async() & 0xff) # FIXME: m-labs/artiq#713
|
return int(self.bus.input_async() & 0xff) # FIXME: m-labs/artiq#713
|
||||||
|
|
||||||
@kernel
|
|
||||||
def write_config(self, reset=0, clk2x=0, enable=1,
|
|
||||||
trigger=0, aux_miso=0, aux_dac=0b111, board=0xf):
|
|
||||||
"""Set configuration register.
|
|
||||||
|
|
||||||
:param reset: Reset board (auto-clear).
|
|
||||||
:param clk2x: Enable clock double (100 MHz).
|
|
||||||
:param enable: Enable the reading and execution of waveform data from
|
|
||||||
memory.
|
|
||||||
:param trigger: Software trigger, logical OR with ``F1 TTL Input
|
|
||||||
Trigger``.
|
|
||||||
:param aux_miso: Use ``F5 OUT`` for ``MISO``. If ``0``, use the
|
|
||||||
masked logical OR of the DAC channels.
|
|
||||||
:param aux_dac: DAC channel mask to for AUX (``F5 OUT``) output.
|
|
||||||
:param board: Boards to address, ``0xf`` to write to all boards.
|
|
||||||
"""
|
|
||||||
config = ((reset << 0) | (clk2x << 1) | (enable << 2) |
|
|
||||||
(trigger << 3) | (aux_miso << 4) | (aux_dac << 5))
|
|
||||||
self.write_reg(_PDQ_ADR_CONFIG, config, board)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def read_config(self, board=0xf):
|
|
||||||
"""Read configuration register."""
|
|
||||||
return self.read_reg(_PDQ_ADR_CONFIG, board)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def write_crc(self, crc, board=0xf):
|
|
||||||
"""Write checksum register."""
|
|
||||||
self.write_reg(_PDQ_ADR_CRC, crc, board)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def read_crc(self, board=0xf):
|
|
||||||
"""Read checksum register."""
|
|
||||||
return self.read_reg(_PDQ_ADR_CRC, board)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def write_frame(self, frame, board=0xf):
|
|
||||||
"""Write frame selection register."""
|
|
||||||
self.write_reg(_PDQ_ADR_FRAME, frame, board)
|
|
||||||
|
|
||||||
@kernel
|
|
||||||
def read_frame(self, board=0xf):
|
|
||||||
"""Read frame selection register."""
|
|
||||||
return self.read_reg(_PDQ_ADR_FRAME, board)
|
|
||||||
|
|
||||||
@kernel
|
@kernel
|
||||||
def write_mem(self, mem, adr, data, board=0xf): # FIXME: m-labs/artiq#714
|
def write_mem(self, mem, adr, data, board=0xf): # FIXME: m-labs/artiq#714
|
||||||
"""Write to DAC channel waveform data memory.
|
"""Write to DAC channel waveform data memory.
|
||||||
|
@ -158,7 +96,7 @@ class PDQ:
|
||||||
to all boards.
|
to all boards.
|
||||||
"""
|
"""
|
||||||
self.bus.set_xfer(self.chip_select, 24, 0)
|
self.bus.set_xfer(self.chip_select, 24, 0)
|
||||||
self.bus.write((_PDQ_CMD(board, 1, mem, 1) << 24) |
|
self.bus.write((PDQ_CMD(board, 1, mem, 1) << 24) |
|
||||||
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
||||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
||||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
self.bus.set_xfer(self.chip_select, 16, 0)
|
||||||
|
@ -182,7 +120,7 @@ class PDQ:
|
||||||
if not n:
|
if not n:
|
||||||
return
|
return
|
||||||
self.bus.set_xfer(self.chip_select, 24, 8)
|
self.bus.set_xfer(self.chip_select, 24, 8)
|
||||||
self.bus.write((_PDQ_CMD(board, 1, mem, 0) << 24) |
|
self.bus.write((PDQ_CMD(board, 1, mem, 0) << 24) |
|
||||||
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
((adr & 0x00ff) << 16) | (adr & 0xff00))
|
||||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
||||||
self.bus.set_xfer(self.chip_select, 0, 16)
|
self.bus.set_xfer(self.chip_select, 0, 16)
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
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
|
|
@ -22,480 +22,24 @@ import struct
|
||||||
import serial
|
import serial
|
||||||
|
|
||||||
from artiq.wavesynth.coefficients import discrete_compensate
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def discrete_compensate(c):
|
|
||||||
"""Compensate spline coefficients for discrete accumulators.
|
|
||||||
|
|
||||||
Given continuous-time b-spline coefficients, this function
|
|
||||||
compensates for the effect of discrete time steps in the
|
|
||||||
target devices.
|
|
||||||
|
|
||||||
The compensation is performed in-place.
|
|
||||||
"""
|
|
||||||
l = len(c)
|
|
||||||
if l > 2:
|
|
||||||
c[1] += c[2]/2.
|
|
||||||
if l > 3:
|
|
||||||
c[1] += c[3]/6.
|
|
||||||
c[2] += c[3]
|
|
||||||
if l > 4:
|
|
||||||
raise ValueError("Only splines up to cubic order are supported.")
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
crc8 = CRC(0x107)
|
crc8 = CRC(0x107)
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
class PDQ(PDQBase):
|
||||||
"""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
|
|
||||||
|
|
||||||
|
|
||||||
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)]
|
|
||||||
|
|
||||||
def get_num_boards(self):
|
|
||||||
return self.num_boards
|
|
||||||
|
|
||||||
def get_num_channels(self):
|
|
||||||
return self.num_channels
|
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
def _cmd(self, board, is_mem, adr, we):
|
|
||||||
return (adr << 0) | (is_mem << 2) | (board << 3) | (we << 7)
|
|
||||||
|
|
||||||
def write(self, data):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def write_reg(self, board, adr, data):
|
|
||||||
"""Write to a configuration 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", self._cmd(board, False, adr, True), data))
|
|
||||||
|
|
||||||
def set_config(self, reset=False, clk2x=False, enable=True,
|
|
||||||
trigger=False, aux_miso=False, 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 channel data parsers and spline
|
|
||||||
interpolators.
|
|
||||||
trigger (bool): Soft trigger. Logical or with the hardware trigger.
|
|
||||||
aux_miso (bool): Drive SPI MISO on the AUX/F5 ttl port of each
|
|
||||||
board. If `False`, drive the masked logical or of the DAC
|
|
||||||
channels' aux data.
|
|
||||||
aux_dac (int): 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.
|
|
||||||
"""
|
|
||||||
self.write_reg(board, 0, (reset << 0) | (clk2x << 1) | (enable << 2) |
|
|
||||||
(trigger << 3) | (aux_miso << 4) | (aux_dac << 5))
|
|
||||||
|
|
||||||
def set_checksum(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.write_reg(board, 1, crc)
|
|
||||||
|
|
||||||
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.write_reg(board, 2, frame)
|
|
||||||
|
|
||||||
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", self._cmd(board, True, dac, True),
|
|
||||||
start_addr) + data)
|
|
||||||
|
|
||||||
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 disable(self, **kwargs):
|
|
||||||
"""Disable the device."""
|
|
||||||
self.set_config(enable=False, **kwargs)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def enable(self, **kwargs):
|
|
||||||
"""Enable the device."""
|
|
||||||
self.set_config(enable=True, **kwargs)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def ping(self):
|
|
||||||
"""Ping method returning True. Required for ARTIQ remote
|
|
||||||
controller."""
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Pdq(PdqBase):
|
|
||||||
def __init__(self, url=None, dev=None, **kwargs):
|
def __init__(self, url=None, dev=None, **kwargs):
|
||||||
"""Initialize PDQ USB/Parallel device stack.
|
"""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:
|
Args:
|
||||||
url (str): Pyserial device URL. Can be ``hwgrep://`` style
|
url (str): Pyserial device URL. Can be ``hwgrep://`` style
|
||||||
(search for serial number, bus topology, USB VID:PID
|
(search for serial number, bus topology, USB VID:PID
|
||||||
|
@ -503,12 +47,13 @@ class Pdq(PdqBase):
|
||||||
``/dev/ttyUSB0`` for a Linux serial port.
|
``/dev/ttyUSB0`` for a Linux serial port.
|
||||||
dev (file-like): File handle to use as device. If passed, ``url``
|
dev (file-like): File handle to use as device. If passed, ``url``
|
||||||
is ignored.
|
is ignored.
|
||||||
**kwargs: See :class:`PdqBase` .
|
**kwargs: See :class:`PDQBase` .
|
||||||
"""
|
"""
|
||||||
if dev is None:
|
if dev is None:
|
||||||
dev = serial.serial_for_url(url)
|
dev = serial.serial_for_url(url)
|
||||||
self.dev = dev
|
self.dev = dev
|
||||||
PdqBase.__init__(self, **kwargs)
|
self.crc = 0
|
||||||
|
PDQBase.__init__(self, **kwargs)
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
"""Write data to the PDQ board over USB/parallel.
|
"""Write data to the PDQ board over USB/parallel.
|
||||||
|
@ -524,7 +69,31 @@ class Pdq(PdqBase):
|
||||||
written = self.dev.write(msg)
|
written = self.dev.write(msg)
|
||||||
if isinstance(written, int):
|
if isinstance(written, int):
|
||||||
assert written == len(msg), (written, len(msg))
|
assert written == len(msg), (written, len(msg))
|
||||||
self.checksum = crc8(data, self.checksum)
|
self.crc = crc8(data, self.crc)
|
||||||
|
|
||||||
|
def write_reg(self, adr, data, board):
|
||||||
|
"""Write to a configuration 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):
|
def close(self):
|
||||||
"""Close the USB device handle."""
|
"""Close the USB device handle."""
|
||||||
|
@ -534,25 +103,3 @@ class Pdq(PdqBase):
|
||||||
def flush(self):
|
def flush(self):
|
||||||
"""Flush pending data."""
|
"""Flush pending data."""
|
||||||
self.dev.flush()
|
self.dev.flush()
|
||||||
|
|
||||||
|
|
||||||
class PdqSPI(PdqBase):
|
|
||||||
def __init__(self, dev=None, **kwargs):
|
|
||||||
"""Initialize PDQ SPI device stack."""
|
|
||||||
self.dev = dev
|
|
||||||
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)
|
|
||||||
written = self.dev.write(data)
|
|
||||||
if isinstance(written, int):
|
|
||||||
assert written == len(data), (written, len(data))
|
|
||||||
self.checksum = crc8(data, self.checksum)
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
from artiq.language import *
|
from artiq.language import *
|
||||||
|
|
||||||
|
|
||||||
frame_setup = 20*ns
|
frame_setup = 1.5*us
|
||||||
trigger_duration = 50*ns
|
|
||||||
sample_period = 10*ns
|
sample_period = 10*ns
|
||||||
delay_margin_factor = 1 + 1e-4
|
delay_margin_factor = 1 + 1e-4
|
||||||
|
|
||||||
|
@ -117,7 +116,7 @@ class _Frame:
|
||||||
r += segment_program
|
r += segment_program
|
||||||
return r
|
return r
|
||||||
|
|
||||||
@kernel
|
@portable
|
||||||
def advance(self):
|
def advance(self):
|
||||||
if self.invalidated:
|
if self.invalidated:
|
||||||
raise InvalidatedError()
|
raise InvalidatedError()
|
||||||
|
@ -137,9 +136,7 @@ class _Frame:
|
||||||
self.pdq.current_frame = self.frame_number
|
self.pdq.current_frame = self.frame_number
|
||||||
self.pdq.next_segment = 0
|
self.pdq.next_segment = 0
|
||||||
at_mu(trigger_start_t - self.core.seconds_to_mu(frame_setup))
|
at_mu(trigger_start_t - self.core.seconds_to_mu(frame_setup))
|
||||||
self.pdq.frame0.set_o(bool(self.frame_number & 1))
|
self.pdq.write_frame(self.frame_number)
|
||||||
self.pdq.frame1.set_o(bool((self.frame_number & 2) >> 1))
|
|
||||||
self.pdq.frame2.set_o(bool((self.frame_number & 4) >> 2))
|
|
||||||
|
|
||||||
at_mu(trigger_start_t)
|
at_mu(trigger_start_t)
|
||||||
self.pdq.trigger.pulse(trigger_duration)
|
self.pdq.trigger.pulse(trigger_duration)
|
||||||
|
@ -155,30 +152,34 @@ class _Frame:
|
||||||
|
|
||||||
|
|
||||||
class CompoundPDQ:
|
class CompoundPDQ:
|
||||||
def __init__(self, dmgr, pdq_devices, trigger_device, frame_devices):
|
def __init__(self, dmgr, pdq_devices, trigger_device,
|
||||||
|
aux_miso=0, aux_dac=0b111, clk2x=0):
|
||||||
self.core = dmgr.get("core")
|
self.core = dmgr.get("core")
|
||||||
self.pdqs = [dmgr.get(d) for d in pdq_devices]
|
self.pdqs = [dmgr.get(d) for d in pdq_devices]
|
||||||
self.trigger = dmgr.get(trigger_device)
|
self.trigger = dmgr.get(trigger_device)
|
||||||
self.frame0 = dmgr.get(frame_devices[0])
|
self.aux_miso = aux_miso
|
||||||
self.frame1 = dmgr.get(frame_devices[1])
|
self.aux_dac = aux_dac
|
||||||
self.frame2 = dmgr.get(frame_devices[2])
|
self.clk2x = clk2x
|
||||||
|
|
||||||
self.frames = []
|
self.frames = []
|
||||||
self.current_frame = -1
|
self.current_frame = -1
|
||||||
self.next_segment = -1
|
self.next_segment = -1
|
||||||
self.armed = False
|
self.armed = False
|
||||||
|
|
||||||
|
@portable
|
||||||
def disarm(self):
|
def disarm(self):
|
||||||
for frame in self.frames:
|
for frame in self.frames:
|
||||||
frame._invalidate()
|
frame._invalidate()
|
||||||
self.frames.clear()
|
self.frames.clear()
|
||||||
for dev in self.pdqs:
|
for dev in self.pdqs:
|
||||||
dev.park()
|
dev.write_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
|
self.armed = False
|
||||||
|
|
||||||
def get_program(self):
|
def get_program(self):
|
||||||
return [f._get_program() for f in self.frames]
|
return [f._get_program() for f in self.frames]
|
||||||
|
|
||||||
|
@portable
|
||||||
def arm(self):
|
def arm(self):
|
||||||
if self.armed:
|
if self.armed:
|
||||||
raise ArmError()
|
raise ArmError()
|
||||||
|
@ -204,7 +205,8 @@ class CompoundPDQ:
|
||||||
pdq.program(program)
|
pdq.program(program)
|
||||||
n += dn
|
n += dn
|
||||||
for pdq in self.pdqs:
|
for pdq in self.pdqs:
|
||||||
pdq.unpark()
|
dev.write_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
|
self.armed = True
|
||||||
|
|
||||||
def create_frame(self):
|
def create_frame(self):
|
||||||
|
@ -213,3 +215,8 @@ class CompoundPDQ:
|
||||||
r = _Frame(self, len(self.frames))
|
r = _Frame(self, len(self.frames))
|
||||||
self.frames.append(r)
|
self.frames.append(r)
|
||||||
return r
|
return r
|
||||||
|
|
||||||
|
@kernel
|
||||||
|
def write_frame(self, frame):
|
||||||
|
for pdq in self.pdqs:
|
||||||
|
pdq.write_frame(self.frame_number)
|
||||||
|
|
|
@ -0,0 +1,446 @@
|
||||||
|
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 write_reg(self, adr, data, board):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def read_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 write_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.write_reg(PDQ_ADR_CONFIG, config, board)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def read_config(self, board=0xf):
|
||||||
|
"""Read configuration register.
|
||||||
|
|
||||||
|
.. seealso: :meth:`write_config`
|
||||||
|
"""
|
||||||
|
return self.read_reg(PDQ_ADR_CONFIG, board)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def write_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.write_reg(PDQ_ADR_CRC, crc, board)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def read_crc(self, board=0xf):
|
||||||
|
"""Read checksum register.
|
||||||
|
|
||||||
|
.. seealso:: :meth:`write_crc`
|
||||||
|
"""
|
||||||
|
return self.read_reg(PDQ_ADR_CRC, board)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def write_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.write_reg(PDQ_ADR_FRAME, frame, board)
|
||||||
|
|
||||||
|
@portable
|
||||||
|
def read_frame(self, board=0xf):
|
||||||
|
"""Read frame selection register.
|
||||||
|
|
||||||
|
.. seealso:: :meth:`write_frame`
|
||||||
|
"""
|
||||||
|
return self.read_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
|
|
@ -4,13 +4,15 @@ import argparse
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from artiq.devices.pdq.driver import Pdq
|
from artiq.devices.pdq.driver import PDQ
|
||||||
from artiq.protocols.pc_rpc import simple_server_loop
|
from artiq.protocols.pc_rpc import simple_server_loop
|
||||||
from artiq.tools import *
|
from artiq.tools import *
|
||||||
|
|
||||||
|
|
||||||
def get_argparser():
|
def get_argparser():
|
||||||
parser = argparse.ArgumentParser(description="PDQ controller")
|
parser = argparse.ArgumentParser(description="""PDQ controller.
|
||||||
|
|
||||||
|
Use this controller for PDQ stacks that are connected via USB.""")
|
||||||
simple_network_args(parser, 3252)
|
simple_network_args(parser, 3252)
|
||||||
parser.add_argument("-d", "--device", default=None, help="serial port")
|
parser.add_argument("-d", "--device", default=None, help="serial port")
|
||||||
parser.add_argument("--simulation", action="store_true",
|
parser.add_argument("--simulation", action="store_true",
|
||||||
|
@ -37,14 +39,14 @@ def main():
|
||||||
|
|
||||||
if args.simulation:
|
if args.simulation:
|
||||||
port = open(args.dump, "wb")
|
port = open(args.dump, "wb")
|
||||||
dev = Pdq(url=args.device, dev=port, num_boards=args.boards)
|
dev = PDQ(url=args.device, dev=port, num_boards=args.boards)
|
||||||
try:
|
try:
|
||||||
if args.reset:
|
if args.reset:
|
||||||
dev.write(b"") # flush eop
|
dev.write(b"") # flush eop
|
||||||
dev.set_config(reset=True)
|
dev.write_config(reset=True)
|
||||||
time.sleep(.1)
|
time.sleep(.1)
|
||||||
|
|
||||||
dev.set_checksum(0)
|
dev.write_crc(0)
|
||||||
dev.checksum = 0
|
dev.checksum = 0
|
||||||
|
|
||||||
simple_server_loop({"pdq": dev}, bind_address_from_args(args),
|
simple_server_loop({"pdq": dev}, bind_address_from_args(args),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import unittest
|
||||||
import os
|
import os
|
||||||
import io
|
import io
|
||||||
|
|
||||||
from artiq.devices.pdq.driver import Pdq
|
from artiq.devices.pdq.driver import PDQ
|
||||||
from artiq.wavesynth.compute_samples import Synthesizer
|
from artiq.wavesynth.compute_samples import Synthesizer
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,11 +13,11 @@ pdq_gateware = os.getenv("ARTIQ_PDQ_GATEWARE")
|
||||||
|
|
||||||
class TestPdq(unittest.TestCase):
|
class TestPdq(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.dev = Pdq(dev=io.BytesIO())
|
self.dev = PDQ(dev=io.BytesIO())
|
||||||
self.synth = Synthesizer(3, _test_program)
|
self.synth = Synthesizer(3, _test_program)
|
||||||
|
|
||||||
def test_reset(self):
|
def test_reset(self):
|
||||||
self.dev.set_config(reset=True)
|
self.dev.write_config(reset=True)
|
||||||
buf = self.dev.dev.getvalue()
|
buf = self.dev.dev.getvalue()
|
||||||
self.assertEqual(buf, b"\xa5\x02\xf8\xe5\xa5\x03")
|
self.assertEqual(buf, b"\xa5\x02\xf8\xe5\xa5\x03")
|
||||||
|
|
||||||
|
@ -26,9 +26,9 @@ class TestPdq(unittest.TestCase):
|
||||||
self.dev.program(_test_program)
|
self.dev.program(_test_program)
|
||||||
|
|
||||||
def test_cmd_program(self):
|
def test_cmd_program(self):
|
||||||
self.dev.set_config(enable=False)
|
self.dev.write_config(enable=False)
|
||||||
self.dev.program(_test_program)
|
self.dev.program(_test_program)
|
||||||
self.dev.set_config(enable=True, trigger=True)
|
self.dev.write_config(enable=True, trigger=True)
|
||||||
return self.dev.dev.getvalue()
|
return self.dev.dev.getvalue()
|
||||||
|
|
||||||
def test_synth(self):
|
def test_synth(self):
|
||||||
|
@ -43,10 +43,14 @@ class TestPdq(unittest.TestCase):
|
||||||
from gateware.pdq import PdqSim
|
from gateware.pdq import PdqSim
|
||||||
from migen import run_simulation
|
from migen import run_simulation
|
||||||
|
|
||||||
|
def ncycles(n):
|
||||||
|
for i in range(n):
|
||||||
|
yield
|
||||||
|
|
||||||
buf = self.test_cmd_program()
|
buf = self.test_cmd_program()
|
||||||
tb = PdqSim()
|
tb = PdqSim()
|
||||||
tb.ctrl_pads.trigger.reset = 1
|
tb.ctrl_pads.trigger.reset = 1
|
||||||
run_simulation(tb, ncycles=len(buf) + 250)
|
run_simulation(tb, [ncycles(len(buf) + 250)])
|
||||||
delays = 7, 10, 30
|
delays = 7, 10, 30
|
||||||
y = list(zip(*tb.outputs[len(buf) + 130:]))
|
y = list(zip(*tb.outputs[len(buf) + 130:]))
|
||||||
y = list(zip(*(yi[di:] for yi, di in zip(y, delays))))
|
y = list(zip(*(yi[di:] for yi, di in zip(y, delays))))
|
||||||
|
|
|
@ -11,6 +11,12 @@ Core device logging controller
|
||||||
PDQ2
|
PDQ2
|
||||||
----
|
----
|
||||||
|
|
||||||
|
Protocol
|
||||||
|
++++++++
|
||||||
|
|
||||||
|
.. automodule:: artiq.devices.pdq.protocol
|
||||||
|
:members:
|
||||||
|
|
||||||
Driver
|
Driver
|
||||||
++++++
|
++++++
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue