pdq: get new host driver, adapt

This commit is contained in:
Robert Jördens 2017-05-31 00:20:10 +02:00
parent 2895448477
commit 2458da1ade
8 changed files with 269 additions and 102 deletions

View File

@ -0,0 +1 @@
from artiq.devices.pdq.mediator import *

View File

@ -1,4 +1,19 @@
# Copyright (C) 2012-2015 Robert Jordens <jordens@gmail.com> # Copyright 2013-2017 Robert Jordens <jordens@gmail.com>
#
# 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 <http://www.gnu.org/licenses/>.
from math import log, sqrt from math import log, sqrt
import logging import logging
@ -12,6 +27,65 @@ from artiq.wavesynth.coefficients import discrete_compensate
logger = logging.getLogger(__name__) 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: class Segment:
"""Serialize the lines for a single Segment. """Serialize the lines for a single Segment.
@ -49,6 +123,8 @@ class Segment:
this line. this line.
silence (bool): Disable DAC clocks for the duration of this line. silence (bool): Disable DAC clocks for the duration of this line.
aux (bool): Assert the AUX (F5 TTL) output during 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. shift (int): Duration and spline evolution exponent.
jump (bool): Return to the frame address table after this line. jump (bool): Return to the frame address table after this line.
clear (bool): Clear the DDS phase accumulator when starting to clear (bool): Clear the DDS phase accumulator when starting to
@ -134,17 +210,16 @@ class Segment:
class Channel: class Channel:
"""PDQ2 Channel. """PDQ Channel.
Attributes: Attributes:
num_frames (int): Number of frames supported. num_frames (int): Number of frames supported.
max_data (int): Number of 16 bit data words per channel. max_data (int): Number of 16 bit data words per channel.
segments (list[Segment]): Segments added to this channel. segments (list[Segment]): Segments added to this channel.
""" """
num_frames = 8 def __init__(self, max_data, num_frames):
max_data = 4*(1 << 10) # 8kx16 8kx16 4kx16 self.max_data = max_data
self.num_frames = num_frames
def __init__(self):
self.segments = [] self.segments = []
def clear(self): def clear(self):
@ -220,38 +295,39 @@ class Channel:
return self.table(entry) + data return self.table(entry) + data
class Pdq2: class PdqBase:
""" """
PDQ stack. 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: 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_channels (int): Number of channels in this stack.
num_boards (int): Number of boards 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. channels (list[Channel]): List of :class:`Channel` in this stack.
""" """
num_dacs = 3
freq = 50e6 freq = 50e6
_escape = b"\xa5" _mem_sizes = [None, (20,), (10, 10), (8, 6, 6)] # 10kx16 units
_commands = "RESET TRIGGER ARM DCM START".split()
def __init__(self, url=None, dev=None, num_boards=3): def __init__(self, num_boards=3, num_dacs=3, num_frames=32):
if dev is None: """Initialize PDQ stack.
dev = serial.serial_for_url(url)
self.dev = dev 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_boards = num_boards
self.num_dacs = num_dacs
self.num_frames = num_frames
self.num_channels = self.num_dacs * self.num_boards 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): def get_num_boards(self):
return self.num_boards return self.num_boards
@ -259,41 +335,71 @@ class Pdq2:
def get_num_channels(self): def get_num_channels(self):
return self.num_channels return self.num_channels
def get_num_frames(self):
return self.num_frames
def get_freq(self): def get_freq(self):
return self.freq return self.freq
def set_freq(self, freq): def set_freq(self, freq):
self.freq = float(freq) self.freq = float(freq)
def close(self): def _cmd(self, board, is_mem, adr, we):
"""Close the USB device handle.""" return (adr << 0) | (is_mem << 2) | (board << 3) | (we << 7)
self.dev.close()
del self.dev
def write(self, data): 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: 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) self.write(struct.pack(
written = self.dev.write(data) "<BB", self._cmd(board, False, adr, True), data))
if isinstance(written, int):
assert written == len(data)
def cmd(self, cmd, enable): def set_config(self, reset=False, clk2x=False, enable=True,
"""Execute a command. trigger=False, aux_miso=False, aux_dac=0b111, board=0xf):
"""Set the configuration register.
Args: Args:
cmd (str): Command to execute. One of (``RESET``, ``TRIGGER``, reset (bool): Reset the board. Memory is not reset. Self-clearing.
``ARM``, ``DCM``, ``START``). clk2x (bool): Enable the clock multiplier (100 MHz instead of 50
enable (bool): Enable (``True``) or disable (``False``) the MHz)
feature. 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.
""" """
cmd = self._commands.index(cmd) << 1 self.write_reg(board, 0, (reset << 0) | (clk2x << 1) | (enable << 2) |
if not enable: (trigger << 3) | (aux_miso << 4) | (aux_dac << 5))
cmd |= 1
self.write(struct.pack("cb", self._escape, cmd)) 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): def write_mem(self, channel, data, start_addr=0):
"""Write to channel memory. """Write to channel memory.
@ -305,10 +411,8 @@ class Pdq2:
start_addr (int): Start address to write data to. 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, self.write(struct.pack("<BH", self._cmd(board, True, dac, True),
start_addr + len(data)//2 - 1) + data start_addr) + data)
data = data.replace(self._escape, self._escape + self._escape)
self.write(data)
def program_segments(self, segments, data): def program_segments(self, segments, data):
"""Append the wavesynth lines to the given segments. """Append the wavesynth lines to the given segments.
@ -372,18 +476,83 @@ 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): def disable(self, **kwargs):
self.dev.flush() """Disable the device."""
self.set_config(enable=False, **kwargs)
def park(self):
self.cmd("START", False)
self.cmd("TRIGGER", True)
self.flush() self.flush()
def unpark(self): def enable(self, **kwargs):
self.cmd("TRIGGER", False) """Enable the device."""
self.cmd("START", True) self.set_config(enable=True, **kwargs)
self.flush() self.flush()
def ping(self): def ping(self):
"""Ping method returning True. Required for ARTIQ remote
controller."""
return True return True
class Pdq(PdqBase):
def __init__(self, url=None, dev=None, **kwargs):
"""Initialize PDQ USB/Parallel device 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.
**kwargs: See :class:`PdqBase` .
"""
if dev is None:
dev = serial.serial_for_url(url)
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)
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)

View File

@ -154,10 +154,10 @@ class _Frame:
self.pdq.next_segment = -1 self.pdq.next_segment = -1
class CompoundPDQ2: class CompoundPDQ:
def __init__(self, dmgr, pdq2_devices, trigger_device, frame_devices): def __init__(self, dmgr, pdq_devices, trigger_device, frame_devices):
self.core = dmgr.get("core") 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.trigger = dmgr.get(trigger_device)
self.frame0 = dmgr.get(frame_devices[0]) self.frame0 = dmgr.get(frame_devices[0])
self.frame1 = dmgr.get(frame_devices[1]) self.frame1 = dmgr.get(frame_devices[1])
@ -172,7 +172,7 @@ class CompoundPDQ2:
for frame in self.frames: for frame in self.frames:
frame._invalidate() frame._invalidate()
self.frames.clear() self.frames.clear()
for dev in self.pdq2s: for dev in self.pdqs:
dev.park() dev.park()
self.armed = False self.armed = False
@ -187,8 +187,8 @@ class CompoundPDQ2:
full_program = self.get_program() full_program = self.get_program()
n = 0 n = 0
for pdq2 in self.pdq2s: for pdq in self.pdqs:
dn = pdq2.get_num_channels() dn = pdq.get_num_channels()
program = [] program = []
for full_frame_program in full_program: for full_frame_program in full_program:
frame_program = [] frame_program = []
@ -201,10 +201,10 @@ class CompoundPDQ2:
} }
frame_program.append(line) frame_program.append(line)
program.append(frame_program) program.append(frame_program)
pdq2.program(program) pdq.program(program)
n += dn n += dn
for pdq2 in self.pdq2s: for pdq in self.pdqs:
pdq2.unpark() pdq.unpark()
self.armed = True self.armed = True
def create_frame(self): def create_frame(self):

View File

@ -1 +0,0 @@
from artiq.devices.pdq2.mediator import *

View File

@ -172,30 +172,30 @@ device_db = {
# that it always resolves to a network-visible IP address (see documentation). # that it always resolves to a network-visible IP address (see documentation).
"host": "::1", "host": "::1",
"port": 4000, "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": { "qc_q1_1": {
"type": "controller", "type": "controller",
"host": "::1", "host": "::1",
"port": 4001, "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": { "qc_q1_2": {
"type": "controller", "type": "controller",
"host": "::1", "host": "::1",
"port": 4002, "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": { "qc_q1_3": {
"type": "controller", "type": "controller",
"host": "::1", "host": "::1",
"port": 4003, "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": { "electrodes": {
"type": "local", "type": "local",
"module": "artiq.devices.pdq2", "module": "artiq.devices.pdq",
"class": "CompoundPDQ2", "class": "CompoundPDQ",
"arguments": { "arguments": {
"pdq2_devices": ["qc_q1_0", "qc_q1_1", "qc_q1_2", "qc_q1_3"], "pdq2_devices": ["qc_q1_0", "qc_q1_1", "qc_q1_2", "qc_q1_3"],
"trigger_device": "ttl2", "trigger_device": "ttl2",

View File

@ -4,18 +4,18 @@ import argparse
import sys import sys
import time 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.protocols.pc_rpc import simple_server_loop
from artiq.tools import * from artiq.tools import *
def get_argparser(): def get_argparser():
parser = argparse.ArgumentParser(description="PDQ2 controller") parser = argparse.ArgumentParser(description="PDQ controller")
simple_network_args(parser, 3252) simple_network_args(parser, 3252)
parser.add_argument("-d", "--device", default=None, help="serial port") parser.add_argument("-d", "--device", default=None, help="serial port")
parser.add_argument("--simulation", action="store_true", parser.add_argument("--simulation", action="store_true",
help="do not open any device but dump data") 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") help="file to dump simulation data into")
parser.add_argument("-r", "--reset", default=False, parser.add_argument("-r", "--reset", default=False,
action="store_true", help="reset device [%(default)s]") action="store_true", help="reset device [%(default)s]")
@ -37,16 +37,17 @@ def main():
if args.simulation: if args.simulation:
port = open(args.dump, "wb") 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: try:
if args.reset: if args.reset:
dev.write(b"\x00\x00") # flush any escape dev.write(b"") # flush eop
dev.cmd("RESET", True) dev.set_config(reset=True)
dev.flush()
time.sleep(.1) time.sleep(.1)
dev.cmd("ARM", True)
dev.park() dev.set_checksum(0)
simple_server_loop({"pdq2": dev}, bind_address_from_args(args), dev.checksum = 0
simple_server_loop({"pdq": dev}, bind_address_from_args(args),
args.port, description="device=" + str(args.device)) args.port, description="device=" + str(args.device))
finally: finally:
dev.close() dev.close()

View File

@ -4,34 +4,31 @@ import unittest
import os import os
import io import io
from artiq.devices.pdq2.driver import Pdq2 from artiq.devices.pdq.driver import Pdq
from artiq.wavesynth.compute_samples import Synthesizer 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): def setUp(self):
self.dev = Pdq2(dev=io.BytesIO()) self.dev = Pdq(dev=io.BytesIO())
self.synth = Synthesizer(3, _test_program) self.synth = Synthesizer(3, _test_program)
def test_reset(self): def test_reset(self):
self.dev.cmd("RESET", True) self.dev.set_config(reset=True)
buf = self.dev.dev.getvalue() 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): def test_program(self):
# about 0.14 ms # about 0.14 ms
self.dev.program(_test_program) self.dev.program(_test_program)
def test_cmd_program(self): def test_cmd_program(self):
self.dev.cmd("ARM", False) self.dev.set_config(enable=False)
self.dev.cmd("START", False)
self.dev.program(_test_program) self.dev.program(_test_program)
self.dev.cmd("START", True) self.dev.set_config(enable=True, trigger=True)
self.dev.cmd("ARM", True)
# self.dev.cmd("TRIGGER", True)
return self.dev.dev.getvalue() return self.dev.dev.getvalue()
def test_synth(self): def test_synth(self):
@ -42,12 +39,12 @@ class TestPdq2(unittest.TestCase):
def run_gateware(self): def run_gateware(self):
import sys import sys
sys.path.append(pdq2_gateware) sys.path.append(pdq_gateware)
from gateware.pdq2 import Pdq2Sim from gateware.pdq import PdqSim
from migen.sim.generic import run_simulation from migen import run_simulation
buf = self.test_cmd_program() buf = self.test_cmd_program()
tb = Pdq2Sim(buf) tb = PdqSim()
tb.ctrl_pads.trigger.reset = 1 tb.ctrl_pads.trigger.reset = 1
run_simulation(tb, ncycles=len(buf) + 250) run_simulation(tb, ncycles=len(buf) + 250)
delays = 7, 10, 30 delays = 7, 10, 30
@ -57,7 +54,7 @@ class TestPdq2(unittest.TestCase):
self.assertEqual(len(y[0]), 3) self.assertEqual(len(y[0]), 3)
return y return y
@unittest.skipUnless(pdq2_gateware, "no pdq2 gateware") @unittest.skipUnless(pdq_gateware, "no PDQ gateware")
def test_run_compare(self): def test_run_compare(self):
y_ref = self.test_synth() y_ref = self.test_synth()
y = self.run_gateware() y = self.run_gateware()
@ -70,7 +67,7 @@ class TestPdq2(unittest.TestCase):
self.assertAlmostEqual(yij, yij_ref, 2, "disagreement at " self.assertAlmostEqual(yij, yij_ref, 2, "disagreement at "
"t={}, c={}".format(i, j)) "t={}, c={}".format(i, j))
@unittest.skipUnless(pdq2_gateware, "no pdq2 gateware") @unittest.skipUnless(pdq_gateware, "no PDQ gateware")
@unittest.skip("manual/visual test") @unittest.skip("manual/visual test")
def test_run_plot(self): def test_run_plot(self):
from matplotlib import pyplot as plt from matplotlib import pyplot as plt

View File

@ -39,7 +39,7 @@ console_scripts = [
"aqctl_lda=artiq.frontend.aqctl_lda:main", "aqctl_lda=artiq.frontend.aqctl_lda:main",
"aqctl_novatech409b=artiq.frontend.aqctl_novatech409b:main", "aqctl_novatech409b=artiq.frontend.aqctl_novatech409b:main",
"aqctl_korad_ka3005p=artiq.frontend.aqctl_korad_ka3005p: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", "aqctl_thorlabs_tcube=artiq.frontend.aqctl_thorlabs_tcube:main",
] ]