pdq: get new host driver, adapt

pull/668/merge
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
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(
"<BB", self._cmd(board, False, adr, True), data))
def cmd(self, cmd, enable):
"""Execute a command.
def set_config(self, reset=False, clk2x=False, enable=True,
trigger=False, aux_miso=False, aux_dac=0b111, board=0xf):
"""Set the configuration register.
Args:
cmd (str): Command to execute. One of (``RESET``, ``TRIGGER``,
``ARM``, ``DCM``, ``START``).
enable (bool): Enable (``True``) or disable (``False``) the
feature.
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 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
if not enable:
cmd |= 1
self.write(struct.pack("cb", self._escape, cmd))
self.write_reg(board, 0, (reset << 0) | (clk2x << 1) | (enable << 2) |
(trigger << 3) | (aux_miso << 4) | (aux_dac << 5))
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):
"""Write to channel memory.
@ -305,10 +411,8 @@ class Pdq2:
start_addr (int): Start address to write data to.
"""
board, dac = divmod(channel, self.num_dacs)
data = struct.pack("<HHH", (board << 4) | dac, start_addr,
start_addr + len(data)//2 - 1) + data
data = data.replace(self._escape, self._escape + self._escape)
self.write(data)
self.write(struct.pack("<BH", self._cmd(board, True, dac, True),
start_addr) + data)
def program_segments(self, segments, data):
"""Append the wavesynth lines to the given segments.
@ -372,18 +476,83 @@ class Pdq2:
for channel, ch in zip(channels, chs):
self.write_mem(channel, ch.serialize())
def flush(self):
self.dev.flush()
def park(self):
self.cmd("START", False)
self.cmd("TRIGGER", True)
def disable(self, **kwargs):
"""Disable the device."""
self.set_config(enable=False, **kwargs)
self.flush()
def unpark(self):
self.cmd("TRIGGER", False)
self.cmd("START", True)
def enable(self, **kwargs):
"""Enable the device."""
self.set_config(enable=True, **kwargs)
self.flush()
def ping(self):
"""Ping method returning True. Required for ARTIQ remote
controller."""
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
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):

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).
"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",

View File

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

View File

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

View File

@ -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",
]