From 566ff73dff6efc7bd1e8c8d141a619881d448122 Mon Sep 17 00:00:00 2001 From: Robert Jordens Date: Sat, 10 Jun 2017 14:59:03 +0200 Subject: [PATCH] pdq: unify spi-PDQ and usb-PDQ protocols --- artiq/coredevice/pdq.py | 78 +---- artiq/devices/pdq/crc.py | 35 +++ artiq/devices/pdq/driver.py | 525 +++------------------------------- artiq/devices/pdq/mediator.py | 31 +- artiq/devices/pdq/protocol.py | 446 +++++++++++++++++++++++++++++ artiq/frontend/aqctl_pdq.py | 12 +- artiq/test/test_pdq.py | 16 +- doc/manual/ndsp_reference.rst | 6 + 8 files changed, 567 insertions(+), 582 deletions(-) create mode 100644 artiq/devices/pdq/crc.py create mode 100644 artiq/devices/pdq/protocol.py diff --git a/artiq/coredevice/pdq.py b/artiq/coredevice/pdq.py index b5ea70648..a90db725b 100644 --- a/artiq/coredevice/pdq.py +++ b/artiq/coredevice/pdq.py @@ -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) diff --git a/artiq/devices/pdq/crc.py b/artiq/devices/pdq/crc.py new file mode 100644 index 000000000..27469949f --- /dev/null +++ b/artiq/devices/pdq/crc.py @@ -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 diff --git a/artiq/devices/pdq/driver.py b/artiq/devices/pdq/driver.py index fee32b645..55f3e3760 100644 --- a/artiq/devices/pdq/driver.py +++ b/artiq/devices/pdq/driver.py @@ -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(">= 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( - " %r", data) - written = self.dev.write(data) - if isinstance(written, int): - assert written == len(data), (written, len(data)) - self.checksum = crc8(data, self.checksum) diff --git a/artiq/devices/pdq/mediator.py b/artiq/devices/pdq/mediator.py index addb47902..ba49d795c 100644 --- a/artiq/devices/pdq/mediator.py +++ b/artiq/devices/pdq/mediator.py @@ -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) diff --git a/artiq/devices/pdq/protocol.py b/artiq/devices/pdq/protocol.py new file mode 100644 index 000000000..3f28a7ce1 --- /dev/null +++ b/artiq/devices/pdq/protocol.py @@ -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(">= 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 diff --git a/artiq/frontend/aqctl_pdq.py b/artiq/frontend/aqctl_pdq.py index 137bbc2b8..ebf19f243 100755 --- a/artiq/frontend/aqctl_pdq.py +++ b/artiq/frontend/aqctl_pdq.py @@ -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), diff --git a/artiq/test/test_pdq.py b/artiq/test/test_pdq.py index 5effcdd45..47c8ad8ae 100644 --- a/artiq/test/test_pdq.py +++ b/artiq/test/test_pdq.py @@ -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)))) diff --git a/doc/manual/ndsp_reference.rst b/doc/manual/ndsp_reference.rst index ff673dbf2..d1e4176f2 100644 --- a/doc/manual/ndsp_reference.rst +++ b/doc/manual/ndsp_reference.rst @@ -11,6 +11,12 @@ Core device logging controller PDQ2 ---- +Protocol +++++++++ + +.. automodule:: artiq.devices.pdq.protocol + :members: + Driver ++++++