pdq2: sync with pdq2

This commit is contained in:
Robert Jördens 2016-10-17 12:40:10 +02:00
parent 69099691f7
commit 0e41725e2d
1 changed files with 180 additions and 20 deletions

View File

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