forked from M-Labs/artiq
pdq2: sync with pdq2
This commit is contained in:
parent
69099691f7
commit
0e41725e2d
|
@ -13,6 +13,17 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Segment:
|
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_time = 1 << 16 # uint16 timer
|
||||||
max_val = 1 << 15 # int16 DAC
|
max_val = 1 << 15 # int16 DAC
|
||||||
max_out = 10. # Volt
|
max_out = 10. # Volt
|
||||||
|
@ -27,6 +38,24 @@ class Segment:
|
||||||
|
|
||||||
def line(self, typ, duration, data, trigger=False, silence=False,
|
def line(self, typ, duration, data, trigger=False, silence=False,
|
||||||
aux=False, shift=0, jump=False, clear=False, wait=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.
|
||||||
|
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 == 0, data
|
||||||
assert len(data)//2 <= 14
|
assert len(data)//2 <= 14
|
||||||
# assert dt*(1 << shift) > 1 + len(data)//2
|
# assert dt*(1 << shift) > 1 + len(data)//2
|
||||||
|
@ -39,6 +68,15 @@ class Segment:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def pack(widths, values):
|
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 = "<"
|
fmt = "<"
|
||||||
ud = []
|
ud = []
|
||||||
for width, value in zip(widths, values):
|
for width, value in zip(widths, values):
|
||||||
|
@ -60,7 +98,11 @@ class Segment:
|
||||||
def bias(self, amplitude=[], **kwargs):
|
def bias(self, amplitude=[], **kwargs):
|
||||||
"""Append a bias line to this segment.
|
"""Append a bias line to this segment.
|
||||||
|
|
||||||
Amplitude in volts
|
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]
|
coef = [self.out_scale*a for a in amplitude]
|
||||||
discrete_compensate(coef)
|
discrete_compensate(coef)
|
||||||
|
@ -68,12 +110,18 @@ class Segment:
|
||||||
self.line(typ=0, data=data, **kwargs)
|
self.line(typ=0, data=data, **kwargs)
|
||||||
|
|
||||||
def dds(self, amplitude=[], phase=[], **kwargs):
|
def dds(self, amplitude=[], phase=[], **kwargs):
|
||||||
"""Append a dds line to this segment.
|
"""Append a DDS line to this segment.
|
||||||
|
|
||||||
Amplitude in volts,
|
Args:
|
||||||
phase[0] in turns,
|
amplitude (list[float]): Amplitude coefficients in in Volts and
|
||||||
phase[1] in turns*sample_rate,
|
increasing powers of ``1/(2**shift*clock_period)``.
|
||||||
phase[2] in turns*(sample_rate/2**shift)**2
|
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
|
scale = self.out_scale/self.cordic_gain
|
||||||
coef = [scale*a for a in amplitude]
|
coef = [scale*a for a in amplitude]
|
||||||
|
@ -86,6 +134,13 @@ class Segment:
|
||||||
|
|
||||||
|
|
||||||
class Channel:
|
class Channel:
|
||||||
|
"""PDQ2 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.
|
||||||
|
"""
|
||||||
num_frames = 8
|
num_frames = 8
|
||||||
max_data = 4*(1 << 10) # 8kx16 8kx16 4kx16
|
max_data = 4*(1 << 10) # 8kx16 8kx16 4kx16
|
||||||
|
|
||||||
|
@ -93,14 +148,27 @@ class Channel:
|
||||||
self.segments = []
|
self.segments = []
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
|
"""Remove all segments."""
|
||||||
self.segments.clear()
|
self.segments.clear()
|
||||||
|
|
||||||
def new_segment(self):
|
def new_segment(self):
|
||||||
|
"""Create and attach a new :class:`Segment` to this channel.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
:class:`Segment`
|
||||||
|
"""
|
||||||
segment = Segment()
|
segment = Segment()
|
||||||
self.segments.append(segment)
|
self.segments.append(segment)
|
||||||
return segment
|
return segment
|
||||||
|
|
||||||
def place(self):
|
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
|
addr = self.num_frames
|
||||||
for segment in self.segments:
|
for segment in self.segments:
|
||||||
segment.addr = addr
|
segment.addr = addr
|
||||||
|
@ -109,6 +177,23 @@ class Channel:
|
||||||
return addr
|
return addr
|
||||||
|
|
||||||
def table(self, entry=None):
|
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
|
table = [0] * self.num_frames
|
||||||
if entry is None:
|
if entry is None:
|
||||||
entry = self.segments
|
entry = self.segments
|
||||||
|
@ -118,6 +203,18 @@ class Channel:
|
||||||
return struct.pack("<" + "H"*self.num_frames, *table)
|
return struct.pack("<" + "H"*self.num_frames, *table)
|
||||||
|
|
||||||
def serialize(self, entry=None):
|
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()
|
self.place()
|
||||||
data = b"".join([segment.data for segment in self.segments])
|
data = b"".join([segment.data for segment in self.segments])
|
||||||
return self.table(entry) + data
|
return self.table(entry) + data
|
||||||
|
@ -125,7 +222,22 @@ class Channel:
|
||||||
|
|
||||||
class Pdq2:
|
class Pdq2:
|
||||||
"""
|
"""
|
||||||
PDQ DAC (a.k.a. QC_Waveform)
|
PDQ stack.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url (str): Pyserial device URL. Can be ``hwgrep://`` style
|
||||||
|
(search for serial number, bus topology, USB VID:PID combination),
|
||||||
|
``COM15`` for a Windows COM port number,
|
||||||
|
``/dev/ttyUSB0`` for a Linux serial port.
|
||||||
|
dev (file-like): File handle to use as device. If passed, ``url`` is
|
||||||
|
ignored.
|
||||||
|
num_boards (int): Number of boards in this stack.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
num_dacs (int): Number of DAC outputs per board.
|
||||||
|
num_channels (int): Number of channels in this stack.
|
||||||
|
num_boards (int): Number of boards in this stack.
|
||||||
|
channels (list[Channel]): List of :class:`Channel` in this stack.
|
||||||
"""
|
"""
|
||||||
num_dacs = 3
|
num_dacs = 3
|
||||||
freq = 50e6
|
freq = 50e6
|
||||||
|
@ -154,42 +266,58 @@ class Pdq2:
|
||||||
self.freq = float(freq)
|
self.freq = float(freq)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
|
"""Close the USB device handle."""
|
||||||
self.dev.close()
|
self.dev.close()
|
||||||
del self.dev
|
del self.dev
|
||||||
|
|
||||||
def write(self, data):
|
def write(self, data):
|
||||||
|
"""Write data to the PDQ2 board.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data (bytes): Data to write.
|
||||||
|
"""
|
||||||
logger.debug("> %r", data)
|
logger.debug("> %r", data)
|
||||||
written = self.dev.write(data)
|
written = self.dev.write(data)
|
||||||
if isinstance(written, int):
|
if isinstance(written, int):
|
||||||
assert written == len(data)
|
assert written == len(data)
|
||||||
|
|
||||||
def cmd(self, cmd, enable):
|
def cmd(self, cmd, enable):
|
||||||
|
"""Execute a command.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd (str): Command to execute. One of (``RESET``, ``TRIGGER``,
|
||||||
|
``ARM``, ``DCM``, ``START``).
|
||||||
|
enable (bool): Enable (``True``) or disable (``False``) the
|
||||||
|
feature.
|
||||||
|
"""
|
||||||
cmd = self._commands.index(cmd) << 1
|
cmd = self._commands.index(cmd) << 1
|
||||||
if not enable:
|
if not enable:
|
||||||
cmd |= 1
|
cmd |= 1
|
||||||
self.write(struct.pack("cb", self._escape, cmd))
|
self.write(struct.pack("cb", self._escape, cmd))
|
||||||
|
|
||||||
def write_mem(self, channel, data, start_addr=0):
|
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)
|
board, dac = divmod(channel, self.num_dacs)
|
||||||
data = struct.pack("<HHH", (board << 4) | dac, start_addr,
|
data = struct.pack("<HHH", (board << 4) | dac, start_addr,
|
||||||
start_addr + len(data)//2 - 1) + data
|
start_addr + len(data)//2 - 1) + data
|
||||||
data = data.replace(self._escape, self._escape + self._escape)
|
data = data.replace(self._escape, self._escape + self._escape)
|
||||||
self.write(data)
|
self.write(data)
|
||||||
|
|
||||||
def flush(self):
|
|
||||||
self.dev.flush()
|
|
||||||
|
|
||||||
def park(self):
|
|
||||||
self.cmd("START", False)
|
|
||||||
self.cmd("TRIGGER", True)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def unpark(self):
|
|
||||||
self.cmd("TRIGGER", False)
|
|
||||||
self.cmd("START", True)
|
|
||||||
self.flush()
|
|
||||||
|
|
||||||
def program_segments(self, segments, 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):
|
for i, line in enumerate(data):
|
||||||
dac_divider = line.get("dac_divider", 1)
|
dac_divider = line.get("dac_divider", 1)
|
||||||
shift = int(log(dac_divider, 2))
|
shift = int(log(dac_divider, 2))
|
||||||
|
@ -208,6 +336,25 @@ class Pdq2:
|
||||||
silence=silence, **target_data)
|
silence=silence, **target_data)
|
||||||
|
|
||||||
def program(self, program, channels=None):
|
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:
|
if channels is None:
|
||||||
channels = range(self.num_channels)
|
channels = range(self.num_channels)
|
||||||
chs = [self.channels[i] for i in channels]
|
chs = [self.channels[i] for i in channels]
|
||||||
|
@ -225,5 +372,18 @@ class Pdq2:
|
||||||
for channel, ch in zip(channels, chs):
|
for channel, ch in zip(channels, chs):
|
||||||
self.write_mem(channel, ch.serialize())
|
self.write_mem(channel, ch.serialize())
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.dev.flush()
|
||||||
|
|
||||||
|
def park(self):
|
||||||
|
self.cmd("START", False)
|
||||||
|
self.cmd("TRIGGER", True)
|
||||||
|
self.flush()
|
||||||
|
|
||||||
|
def unpark(self):
|
||||||
|
self.cmd("TRIGGER", False)
|
||||||
|
self.cmd("START", True)
|
||||||
|
self.flush()
|
||||||
|
|
||||||
def ping(self):
|
def ping(self):
|
||||||
return True
|
return True
|
||||||
|
|
Loading…
Reference in New Issue