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.coredevice import spi
|
||||
from artiq.devices.pdq.protocol import PDQBase, PDQ_CMD
|
||||
|
||||
|
||||
_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
|
||||
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:
|
||||
class PDQ(PDQBase):
|
||||
"""PDQ smart arbitrary waveform generator stack.
|
||||
|
||||
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"}
|
||||
|
||||
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.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):
|
||||
|
@ -82,7 +65,7 @@ class PDQ:
|
|||
: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))
|
||||
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
|
||||
|
@ -96,57 +79,12 @@ class PDQ:
|
|||
:return: Register data (8 bit).
|
||||
"""
|
||||
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
|
||||
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_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
|
||||
def write_mem(self, mem, adr, data, board=0xf): # FIXME: m-labs/artiq#714
|
||||
"""Write to DAC channel waveform data memory.
|
||||
|
@ -158,7 +96,7 @@ class PDQ:
|
|||
to all boards.
|
||||
"""
|
||||
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))
|
||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
||||
self.bus.set_xfer(self.chip_select, 16, 0)
|
||||
|
@ -182,7 +120,7 @@ class PDQ:
|
|||
if not n:
|
||||
return
|
||||
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))
|
||||
delay_mu(-self.bus.write_period_mu-3*self.bus.ref_period_mu)
|
||||
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
|
||||
|
||||
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__)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
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
|
||||
|
@ -503,12 +47,13 @@ class Pdq(PdqBase):
|
|||
``/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` .
|
||||
**kwargs: See :class:`PDQBase` .
|
||||
"""
|
||||
if dev is None:
|
||||
dev = serial.serial_for_url(url)
|
||||
self.dev = dev
|
||||
PdqBase.__init__(self, **kwargs)
|
||||
self.crc = 0
|
||||
PDQBase.__init__(self, **kwargs)
|
||||
|
||||
def write(self, data):
|
||||
"""Write data to the PDQ board over USB/parallel.
|
||||
|
@ -524,7 +69,31 @@ class Pdq(PdqBase):
|
|||
written = self.dev.write(msg)
|
||||
if isinstance(written, int):
|
||||
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):
|
||||
"""Close the USB device handle."""
|
||||
|
@ -534,25 +103,3 @@ class Pdq(PdqBase):
|
|||
def flush(self):
|
||||
"""Flush pending data."""
|
||||
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 *
|
||||
|
||||
|
||||
frame_setup = 20*ns
|
||||
trigger_duration = 50*ns
|
||||
frame_setup = 1.5*us
|
||||
sample_period = 10*ns
|
||||
delay_margin_factor = 1 + 1e-4
|
||||
|
||||
|
@ -117,7 +116,7 @@ class _Frame:
|
|||
r += segment_program
|
||||
return r
|
||||
|
||||
@kernel
|
||||
@portable
|
||||
def advance(self):
|
||||
if self.invalidated:
|
||||
raise InvalidatedError()
|
||||
|
@ -137,9 +136,7 @@ class _Frame:
|
|||
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.frame0.set_o(bool(self.frame_number & 1))
|
||||
self.pdq.frame1.set_o(bool((self.frame_number & 2) >> 1))
|
||||
self.pdq.frame2.set_o(bool((self.frame_number & 4) >> 2))
|
||||
self.pdq.write_frame(self.frame_number)
|
||||
|
||||
at_mu(trigger_start_t)
|
||||
self.pdq.trigger.pulse(trigger_duration)
|
||||
|
@ -155,30 +152,34 @@ class _Frame:
|
|||
|
||||
|
||||
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.pdqs = [dmgr.get(d) for d in pdq_devices]
|
||||
self.trigger = dmgr.get(trigger_device)
|
||||
self.frame0 = dmgr.get(frame_devices[0])
|
||||
self.frame1 = dmgr.get(frame_devices[1])
|
||||
self.frame2 = dmgr.get(frame_devices[2])
|
||||
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.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
|
||||
|
||||
def get_program(self):
|
||||
return [f._get_program() for f in self.frames]
|
||||
|
||||
@portable
|
||||
def arm(self):
|
||||
if self.armed:
|
||||
raise ArmError()
|
||||
|
@ -204,7 +205,8 @@ class CompoundPDQ:
|
|||
pdq.program(program)
|
||||
n += dn
|
||||
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
|
||||
|
||||
def create_frame(self):
|
||||
|
@ -213,3 +215,8 @@ class CompoundPDQ:
|
|||
r = _Frame(self, len(self.frames))
|
||||
self.frames.append(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 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.tools import *
|
||||
|
||||
|
||||
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)
|
||||
parser.add_argument("-d", "--device", default=None, help="serial port")
|
||||
parser.add_argument("--simulation", action="store_true",
|
||||
|
@ -37,14 +39,14 @@ def main():
|
|||
|
||||
if args.simulation:
|
||||
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:
|
||||
if args.reset:
|
||||
dev.write(b"") # flush eop
|
||||
dev.set_config(reset=True)
|
||||
dev.write_config(reset=True)
|
||||
time.sleep(.1)
|
||||
|
||||
dev.set_checksum(0)
|
||||
dev.write_crc(0)
|
||||
dev.checksum = 0
|
||||
|
||||
simple_server_loop({"pdq": dev}, bind_address_from_args(args),
|
||||
|
|
|
@ -4,7 +4,7 @@ import unittest
|
|||
import os
|
||||
import io
|
||||
|
||||
from artiq.devices.pdq.driver import Pdq
|
||||
from artiq.devices.pdq.driver import PDQ
|
||||
from artiq.wavesynth.compute_samples import Synthesizer
|
||||
|
||||
|
||||
|
@ -13,11 +13,11 @@ pdq_gateware = os.getenv("ARTIQ_PDQ_GATEWARE")
|
|||
|
||||
class TestPdq(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.dev = Pdq(dev=io.BytesIO())
|
||||
self.dev = PDQ(dev=io.BytesIO())
|
||||
self.synth = Synthesizer(3, _test_program)
|
||||
|
||||
def test_reset(self):
|
||||
self.dev.set_config(reset=True)
|
||||
self.dev.write_config(reset=True)
|
||||
buf = self.dev.dev.getvalue()
|
||||
self.assertEqual(buf, b"\xa5\x02\xf8\xe5\xa5\x03")
|
||||
|
||||
|
@ -26,9 +26,9 @@ class TestPdq(unittest.TestCase):
|
|||
self.dev.program(_test_program)
|
||||
|
||||
def test_cmd_program(self):
|
||||
self.dev.set_config(enable=False)
|
||||
self.dev.write_config(enable=False)
|
||||
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()
|
||||
|
||||
def test_synth(self):
|
||||
|
@ -43,10 +43,14 @@ class TestPdq(unittest.TestCase):
|
|||
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)
|
||||
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))))
|
||||
|
|
|
@ -11,6 +11,12 @@ Core device logging controller
|
|||
PDQ2
|
||||
----
|
||||
|
||||
Protocol
|
||||
++++++++
|
||||
|
||||
.. automodule:: artiq.devices.pdq.protocol
|
||||
:members:
|
||||
|
||||
Driver
|
||||
++++++
|
||||
|
||||
|
|
Loading…
Reference in New Issue