From 2458da1adebd502431894ffd9bba6200f38b7ae3 Mon Sep 17 00:00:00 2001 From: Robert Jordens Date: Wed, 31 May 2017 00:20:10 +0200 Subject: [PATCH] pdq: get new host driver, adapt --- artiq/devices/pdq/__init__.py | 1 + artiq/devices/{pdq2 => pdq}/driver.py | 285 ++++++++++++++++++----- artiq/devices/{pdq2 => pdq}/mediator.py | 18 +- artiq/devices/pdq2/__init__.py | 1 - artiq/examples/master/device_db.py | 12 +- artiq/frontend/aqctl_pdq2.py | 21 +- artiq/test/{test_pdq2.py => test_pdq.py} | 31 ++- setup.py | 2 +- 8 files changed, 269 insertions(+), 102 deletions(-) create mode 100644 artiq/devices/pdq/__init__.py rename artiq/devices/{pdq2 => pdq}/driver.py (60%) rename artiq/devices/{pdq2 => pdq}/mediator.py (94%) delete mode 100644 artiq/devices/pdq2/__init__.py rename artiq/test/{test_pdq2.py => test_pdq.py} (81%) diff --git a/artiq/devices/pdq/__init__.py b/artiq/devices/pdq/__init__.py new file mode 100644 index 000000000..7d6bbe501 --- /dev/null +++ b/artiq/devices/pdq/__init__.py @@ -0,0 +1 @@ +from artiq.devices.pdq.mediator import * diff --git a/artiq/devices/pdq2/driver.py b/artiq/devices/pdq/driver.py similarity index 60% rename from artiq/devices/pdq2/driver.py rename to artiq/devices/pdq/driver.py index f15928c84..fee32b645 100644 --- a/artiq/devices/pdq2/driver.py +++ b/artiq/devices/pdq/driver.py @@ -1,4 +1,19 @@ -# Copyright (C) 2012-2015 Robert Jordens +# Copyright 2013-2017 Robert Jordens +# +# This file is part of pdq. +# +# pdq is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# pdq is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with pdq. If not, see . from math import log, sqrt import logging @@ -12,6 +27,65 @@ from artiq.wavesynth.coefficients import discrete_compensate 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. @@ -49,6 +123,8 @@ class Segment: 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 @@ -134,17 +210,16 @@ class Segment: class Channel: - """PDQ2 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. """ - num_frames = 8 - max_data = 4*(1 << 10) # 8kx16 8kx16 4kx16 - - def __init__(self): + def __init__(self, max_data, num_frames): + self.max_data = max_data + self.num_frames = num_frames self.segments = [] def clear(self): @@ -220,38 +295,39 @@ class Channel: return self.table(entry) + data -class Pdq2: +class PdqBase: """ 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. + 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. """ - num_dacs = 3 freq = 50e6 - _escape = b"\xa5" - _commands = "RESET TRIGGER ARM DCM START".split() + _mem_sizes = [None, (20,), (10, 10), (8, 6, 6)] # 10kx16 units - def __init__(self, url=None, dev=None, num_boards=3): - if dev is None: - dev = serial.serial_for_url(url) - self.dev = dev + 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 - self.channels = [Channel() for i in range(self.num_channels)] + 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 @@ -259,41 +335,71 @@ class Pdq2: 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 close(self): - """Close the USB device handle.""" - self.dev.close() - del self.dev + def _cmd(self, board, is_mem, adr, we): + return (adr << 0) | (is_mem << 2) | (board << 3) | (we << 7) def write(self, data): - """Write data to the PDQ2 board. + raise NotImplementedError + + def write_reg(self, board, adr, data): + """Write to a configuration register. Args: - data (bytes): Data to write. + 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) """ - logger.debug("> %r", data) - written = self.dev.write(data) - if isinstance(written, int): - assert written == len(data) + self.write(struct.pack( + " %r", data) + msg = b"\xa5\x02" + data.replace(b"\xa5", b"\xa5\xa5") + b"\xa5\x03" + written = self.dev.write(msg) + if isinstance(written, int): + assert written == len(msg), (written, len(msg)) + self.checksum = crc8(data, self.checksum) + + def close(self): + """Close the USB device handle.""" + self.dev.close() + del self.dev + + 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) diff --git a/artiq/devices/pdq2/mediator.py b/artiq/devices/pdq/mediator.py similarity index 94% rename from artiq/devices/pdq2/mediator.py rename to artiq/devices/pdq/mediator.py index 4b4adb5a6..addb47902 100644 --- a/artiq/devices/pdq2/mediator.py +++ b/artiq/devices/pdq/mediator.py @@ -154,10 +154,10 @@ class _Frame: self.pdq.next_segment = -1 -class CompoundPDQ2: - def __init__(self, dmgr, pdq2_devices, trigger_device, frame_devices): +class CompoundPDQ: + def __init__(self, dmgr, pdq_devices, trigger_device, frame_devices): self.core = dmgr.get("core") - self.pdq2s = [dmgr.get(d) for d in pdq2_devices] + 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]) @@ -172,7 +172,7 @@ class CompoundPDQ2: for frame in self.frames: frame._invalidate() self.frames.clear() - for dev in self.pdq2s: + for dev in self.pdqs: dev.park() self.armed = False @@ -187,8 +187,8 @@ class CompoundPDQ2: full_program = self.get_program() n = 0 - for pdq2 in self.pdq2s: - dn = pdq2.get_num_channels() + for pdq in self.pdqs: + dn = pdq.get_num_channels() program = [] for full_frame_program in full_program: frame_program = [] @@ -201,10 +201,10 @@ class CompoundPDQ2: } frame_program.append(line) program.append(frame_program) - pdq2.program(program) + pdq.program(program) n += dn - for pdq2 in self.pdq2s: - pdq2.unpark() + for pdq in self.pdqs: + pdq.unpark() self.armed = True def create_frame(self): diff --git a/artiq/devices/pdq2/__init__.py b/artiq/devices/pdq2/__init__.py deleted file mode 100644 index 10896c0fd..000000000 --- a/artiq/devices/pdq2/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from artiq.devices.pdq2.mediator import * diff --git a/artiq/examples/master/device_db.py b/artiq/examples/master/device_db.py index 0af7c28c1..73f67f04a 100644 --- a/artiq/examples/master/device_db.py +++ b/artiq/examples/master/device_db.py @@ -172,30 +172,30 @@ device_db = { # that it always resolves to a network-visible IP address (see documentation). "host": "::1", "port": 4000, - "command": "aqctl_pdq2 -p {port} --bind {bind} --simulation --dump qc_q1_0.bin" + "command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_0.bin" }, "qc_q1_1": { "type": "controller", "host": "::1", "port": 4001, - "command": "aqctl_pdq2 -p {port} --bind {bind} --simulation --dump qc_q1_1.bin" + "command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_1.bin" }, "qc_q1_2": { "type": "controller", "host": "::1", "port": 4002, - "command": "aqctl_pdq2 -p {port} --bind {bind} --simulation --dump qc_q1_2.bin" + "command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_2.bin" }, "qc_q1_3": { "type": "controller", "host": "::1", "port": 4003, - "command": "aqctl_pdq2 -p {port} --bind {bind} --simulation --dump qc_q1_3.bin" + "command": "aqctl_pdq -p {port} --bind {bind} --simulation --dump qc_q1_3.bin" }, "electrodes": { "type": "local", - "module": "artiq.devices.pdq2", - "class": "CompoundPDQ2", + "module": "artiq.devices.pdq", + "class": "CompoundPDQ", "arguments": { "pdq2_devices": ["qc_q1_0", "qc_q1_1", "qc_q1_2", "qc_q1_3"], "trigger_device": "ttl2", diff --git a/artiq/frontend/aqctl_pdq2.py b/artiq/frontend/aqctl_pdq2.py index a134d8ece..137bbc2b8 100755 --- a/artiq/frontend/aqctl_pdq2.py +++ b/artiq/frontend/aqctl_pdq2.py @@ -4,18 +4,18 @@ import argparse import sys import time -from artiq.devices.pdq2.driver import Pdq2 +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="PDQ2 controller") + parser = argparse.ArgumentParser(description="PDQ controller") simple_network_args(parser, 3252) parser.add_argument("-d", "--device", default=None, help="serial port") parser.add_argument("--simulation", action="store_true", help="do not open any device but dump data") - parser.add_argument("--dump", default="pdq2_dump.bin", + parser.add_argument("--dump", default="pdq_dump.bin", help="file to dump simulation data into") parser.add_argument("-r", "--reset", default=False, action="store_true", help="reset device [%(default)s]") @@ -37,16 +37,17 @@ def main(): if args.simulation: port = open(args.dump, "wb") - dev = Pdq2(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"\x00\x00") # flush any escape - dev.cmd("RESET", True) - dev.flush() + dev.write(b"") # flush eop + dev.set_config(reset=True) time.sleep(.1) - dev.cmd("ARM", True) - dev.park() - simple_server_loop({"pdq2": dev}, bind_address_from_args(args), + + dev.set_checksum(0) + dev.checksum = 0 + + simple_server_loop({"pdq": dev}, bind_address_from_args(args), args.port, description="device=" + str(args.device)) finally: dev.close() diff --git a/artiq/test/test_pdq2.py b/artiq/test/test_pdq.py similarity index 81% rename from artiq/test/test_pdq2.py rename to artiq/test/test_pdq.py index cfec3c7ab..5effcdd45 100644 --- a/artiq/test/test_pdq2.py +++ b/artiq/test/test_pdq.py @@ -4,34 +4,31 @@ import unittest import os import io -from artiq.devices.pdq2.driver import Pdq2 +from artiq.devices.pdq.driver import Pdq from artiq.wavesynth.compute_samples import Synthesizer -pdq2_gateware = os.getenv("ARTIQ_PDQ2_GATEWARE") +pdq_gateware = os.getenv("ARTIQ_PDQ_GATEWARE") -class TestPdq2(unittest.TestCase): +class TestPdq(unittest.TestCase): def setUp(self): - self.dev = Pdq2(dev=io.BytesIO()) + self.dev = Pdq(dev=io.BytesIO()) self.synth = Synthesizer(3, _test_program) def test_reset(self): - self.dev.cmd("RESET", True) + self.dev.set_config(reset=True) buf = self.dev.dev.getvalue() - self.assertEqual(buf, b"\xa5\x00") + self.assertEqual(buf, b"\xa5\x02\xf8\xe5\xa5\x03") def test_program(self): # about 0.14 ms self.dev.program(_test_program) def test_cmd_program(self): - self.dev.cmd("ARM", False) - self.dev.cmd("START", False) + self.dev.set_config(enable=False) self.dev.program(_test_program) - self.dev.cmd("START", True) - self.dev.cmd("ARM", True) - # self.dev.cmd("TRIGGER", True) + self.dev.set_config(enable=True, trigger=True) return self.dev.dev.getvalue() def test_synth(self): @@ -42,12 +39,12 @@ class TestPdq2(unittest.TestCase): def run_gateware(self): import sys - sys.path.append(pdq2_gateware) - from gateware.pdq2 import Pdq2Sim - from migen.sim.generic import run_simulation + sys.path.append(pdq_gateware) + from gateware.pdq import PdqSim + from migen import run_simulation buf = self.test_cmd_program() - tb = Pdq2Sim(buf) + tb = PdqSim() tb.ctrl_pads.trigger.reset = 1 run_simulation(tb, ncycles=len(buf) + 250) delays = 7, 10, 30 @@ -57,7 +54,7 @@ class TestPdq2(unittest.TestCase): self.assertEqual(len(y[0]), 3) return y - @unittest.skipUnless(pdq2_gateware, "no pdq2 gateware") + @unittest.skipUnless(pdq_gateware, "no PDQ gateware") def test_run_compare(self): y_ref = self.test_synth() y = self.run_gateware() @@ -70,7 +67,7 @@ class TestPdq2(unittest.TestCase): self.assertAlmostEqual(yij, yij_ref, 2, "disagreement at " "t={}, c={}".format(i, j)) - @unittest.skipUnless(pdq2_gateware, "no pdq2 gateware") + @unittest.skipUnless(pdq_gateware, "no PDQ gateware") @unittest.skip("manual/visual test") def test_run_plot(self): from matplotlib import pyplot as plt diff --git a/setup.py b/setup.py index 4b8d9665f..94042d905 100755 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ console_scripts = [ "aqctl_lda=artiq.frontend.aqctl_lda:main", "aqctl_novatech409b=artiq.frontend.aqctl_novatech409b:main", "aqctl_korad_ka3005p=artiq.frontend.aqctl_korad_ka3005p:main", - "aqctl_pdq2=artiq.frontend.aqctl_pdq2:main", + "aqctl_pdq=artiq.frontend.aqctl_pdq:main", "aqctl_thorlabs_tcube=artiq.frontend.aqctl_thorlabs_tcube:main", ]