diff --git a/.travis.yml b/.travis.yml index 157ad5aef..d350a0a47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,37 +7,37 @@ branches: sudo: false env: global: - - MSCDIR=$TRAVIS_BUILD_DIR/misoc - - PATH=$HOME/miniconda/bin:/usr/local/llvm-or1k/bin:$PATH - BUILD_SOC=1 - secure: "DUk/Ihg8KbbzEgPF0qrHqlxU8e8eET9i/BtzNvFddIGX4HP/P2qz0nk3cVkmjuWhqJXSbC22RdKME9qqPzw6fJwJ6dpJ3OR6dDmSd7rewavq+niwxu52PVa+yK8mL4yf1terM7QQ5tIRf+yUL9qGKrZ2xyvEuRit6d4cFep43Ws=" before_install: - mkdir -p $HOME/.mlabs - if [ $TRAVIS_PULL_REQUEST != false ]; then BUILD_SOC=0; fi - - . ./.travis/get-toolchain.sh - if [ $BUILD_SOC -ne 0 ]; then ./.travis/get-xilinx.sh; fi - - ./.travis/get-anaconda.sh + - . ./.travis/get-toolchain.sh + - . ./.travis/get-anaconda.sh - source $HOME/miniconda/bin/activate py34 - - conda install pip coverage binstar migen cython + - conda install -q pip coverage binstar migen cython - pip install coveralls install: - conda build conda/artiq - - conda install $HOME/miniconda/conda-bld/linux-64/artiq-*.tar.bz2 + - conda install -q artiq --use-local script: - coverage run --source=artiq setup.py test - make -C doc/manual html after_success: - - binstar login --hostname $(hostname) --username $binstar_login --password $binstar_password - - binstar upload --user $binstar_login --channel dev --force $HOME/miniconda/conda-bld/linux-64/artiq-*.tar.bz2 + - binstar -q login --hostname $(hostname) --username $binstar_login --password $binstar_password + - binstar -q upload --user $binstar_login --channel dev --force $HOME/miniconda/conda-bld/linux-64/artiq-*.tar.bz2 + - binstar -q logout - coveralls notifications: - email: false + email: + recipients: + - rjordens@nist.gov + on_success: always + on_failure: never irc: channels: - chat.freenode.net#m-labs template: - "%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}" - "Build details : %{build_url}" - webhooks: - urls: - - https://webhooks.gitter.im/e/d26782523952bfa53814 diff --git a/.travis/get-anaconda.sh b/.travis/get-anaconda.sh index 992c3d31a..a4c2524b9 100755 --- a/.travis/get-anaconda.sh +++ b/.travis/get-anaconda.sh @@ -1,6 +1,7 @@ #!/bin/sh -wget http://repo.continuum.io/miniconda/Miniconda3-3.7.3-Linux-x86_64.sh -O miniconda.sh +export PATH=$HOME/miniconda/bin:$PATH +wget http://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh bash miniconda.sh -b -p $HOME/miniconda hash -r conda config --set always_yes yes --set changeps1 no @@ -9,4 +10,4 @@ conda info -a conda install conda-build jinja2 conda create -q -n py34 python=$TRAVIS_PYTHON_VERSION conda config --add channels fallen -conda config --add channels https://conda.binstar.org/fallen/channel/dev +conda config --add channels https://conda.anaconda.org/fallen/channel/dev diff --git a/.travis/get-xilinx.sh b/.travis/get-xilinx.sh index c37aead48..95d50e41c 100755 --- a/.travis/get-xilinx.sh +++ b/.travis/get-xilinx.sh @@ -24,12 +24,13 @@ echo "$secret" | gpg --passphrase-fd 0 Xilinx.lic.gpg mkdir -p ~/.Xilinx mv Xilinx.lic ~/.Xilinx/Xilinx.lic -# Tell mibuild where Vivado is installed -echo "MISOC_EXTRA_VIVADO_CMDLINE=\"-Ob vivado_path $HOME/Xilinx/Vivado\"" >> $HOME/.mlabs/build_settings.sh -echo "MISOC_EXTRA_ISE_CMDLINE=\"-Ob ise_path $HOME/opt/Xilinx/\"" >> $HOME/.mlabs/build_settings.sh - -# Lie to Vivado by hooking the ioctl used to retrieve mac address for license verification git clone https://github.com/fallen/impersonate_macaddress make -C impersonate_macaddress -echo "export MACADDR=$macaddress" >> $HOME/.mlabs/build_settings.sh -echo "export LD_PRELOAD=$PWD/impersonate_macaddress/impersonate_macaddress.so" >> $HOME/.mlabs/build_settings.sh +# Tell mibuild where Xilinx toolchains are installed +# and feed it the mac address corresponding to the license +cat > $HOME/.mlabs/build_settings.sh << EOF +MISOC_EXTRA_VIVADO_CMDLINE="-Ob vivado_path $HOME/Xilinx/Vivado" +MISOC_EXTRA_ISE_CMDLINE="-Ob ise_path $HOME/opt/Xilinx/" +export MACADDR=$macaddress +export LD_PRELOAD=$PWD/impersonate_macaddress/impersonate_macaddress.so +EOF diff --git a/artiq/__init__.py b/artiq/__init__.py index 934cfbf49..ae6a8f96b 100644 --- a/artiq/__init__.py +++ b/artiq/__init__.py @@ -1,6 +1,5 @@ -from artiq.language.core import * -from artiq.language.experiment import Experiment -from artiq.language.db import * -from artiq.language.units import check_unit -from artiq.language.units import ps, ns, us, ms, s -from artiq.language.units import Hz, kHz, MHz, GHz +from artiq import language +from artiq.language import * + +__all__ = [] +__all__.extend(language.__all__) diff --git a/artiq/coredevice/comm_dummy.py b/artiq/coredevice/comm_dummy.py index 807f83ca5..5b0c35c46 100644 --- a/artiq/coredevice/comm_dummy.py +++ b/artiq/coredevice/comm_dummy.py @@ -1,21 +1,9 @@ from operator import itemgetter -from artiq.language.db import AutoDB -from artiq.language.units import ms -from artiq.coredevice.runtime import LinkInterface - -class _RuntimeEnvironment(LinkInterface): - def __init__(self): - self.warmup_time = 1*ms - - def emit_object(self): - return str(self.llvm_module) - - -class Comm(AutoDB): - def get_runtime_env(self): - return _RuntimeEnvironment() +class Comm: + def __init__(self, dmgr): + pass def switch_clock(self, external): pass diff --git a/artiq/coredevice/comm_generic.py b/artiq/coredevice/comm_generic.py index afd57bcb8..88fee184b 100644 --- a/artiq/coredevice/comm_generic.py +++ b/artiq/coredevice/comm_generic.py @@ -20,7 +20,12 @@ class _H2DMsgType(Enum): RUN_KERNEL = 5 RPC_REPLY = 6 - + + FLASH_READ_REQUEST = 7 + FLASH_WRITE_REQUEST = 8 + FLASH_ERASE_REQUEST = 9 + FLASH_REMOVE_REQUEST = 10 + class _D2HMsgType(Enum): LOG_REPLY = 1 @@ -37,6 +42,10 @@ class _D2HMsgType(Enum): RPC_REQUEST = 10 + FLASH_READ_REPLY = 11 + FLASH_OK_REPLY = 12 + FLASH_ERROR_REPLY = 13 + class UnsupportedDevice(Exception): pass @@ -86,7 +95,12 @@ class CommGeneric: def _write_header(self, length, ty): self.open() logger.debug("sending message: type=%r length=%d", ty, length) - self.write(struct.pack(">llB", 0x5a5a5a5a, length, ty.value)) + self.write(struct.pack(">ll", 0x5a5a5a5a, length)) + if ty is not None: + self.write(struct.pack("B", ty.value)) + + def reset_session(self): + self._write_header(0, None) def check_ident(self): self._write_header(9, _H2DMsgType.IDENT_REQUEST) @@ -116,12 +130,46 @@ class CommGeneric: if ty != _D2HMsgType.LOAD_COMPLETED: raise IOError("Incorrect reply from device: "+str(ty)) - def run(self, kname, reset_now): - self._write_header(len(kname) + 10, _H2DMsgType.RUN_KERNEL) - self.write(struct.pack("B", reset_now)) + def run(self, kname): + self._write_header(len(kname) + 9, _H2DMsgType.RUN_KERNEL) self.write(bytes(kname, "ascii")) logger.debug("running kernel: %s", kname) + def flash_storage_read(self, key): + self._write_header(9+len(key), _H2DMsgType.FLASH_READ_REQUEST) + self.write(key) + length, ty = self._read_header() + if ty != _D2HMsgType.FLASH_READ_REPLY: + raise IOError("Incorrect reply from device: {}".format(ty)) + value = self.read(length - 9) + return value + + def flash_storage_write(self, key, value): + self._write_header(9+len(key)+1+len(value), + _H2DMsgType.FLASH_WRITE_REQUEST) + self.write(key) + self.write(b"\x00") + self.write(value) + _, ty = self._read_header() + if ty != _D2HMsgType.FLASH_OK_REPLY: + if ty == _D2HMsgType.FLASH_ERROR_REPLY: + raise IOError("Flash storage is full") + else: + raise IOError("Incorrect reply from device: {}".format(ty)) + + def flash_storage_erase(self): + self._write_header(9, _H2DMsgType.FLASH_ERASE_REQUEST) + _, ty = self._read_header() + if ty != _D2HMsgType.FLASH_OK_REPLY: + raise IOError("Incorrect reply from device: {}".format(ty)) + + def flash_storage_remove(self, key): + self._write_header(9+len(key), _H2DMsgType.FLASH_REMOVE_REQUEST) + self.write(key) + _, ty = self._read_header() + if ty != _D2HMsgType.FLASH_OK_REPLY: + raise IOError("Incorrect reply from device: {}".format(ty)) + def _receive_rpc_value(self, type_tag): if type_tag == "n": return None diff --git a/artiq/coredevice/comm_serial.py b/artiq/coredevice/comm_serial.py index 691b43928..70218da14 100644 --- a/artiq/coredevice/comm_serial.py +++ b/artiq/coredevice/comm_serial.py @@ -3,22 +3,22 @@ import serial import struct from artiq.coredevice.comm_generic import CommGeneric -from artiq.language.db import * logger = logging.getLogger(__name__) -class Comm(CommGeneric, AutoDB): - class DBKeys: - serial_dev = Argument() - baud_rate = Argument(115200) +class Comm(CommGeneric): + def __init__(self, dmgr, serial_dev, baud_rate=115200): + self.serial_dev = serial_dev + self.baud_rate = baud_rate def open(self): if hasattr(self, "port"): return self.port = serial.serial_for_url(self.serial_dev, baudrate=self.baud_rate) + self.reset_session() def close(self): if not hasattr(self, "port"): diff --git a/artiq/coredevice/comm_tcp.py b/artiq/coredevice/comm_tcp.py index 4cc254a10..eda672750 100644 --- a/artiq/coredevice/comm_tcp.py +++ b/artiq/coredevice/comm_tcp.py @@ -2,16 +2,15 @@ import logging import socket from artiq.coredevice.comm_generic import CommGeneric -from artiq.language.db import * logger = logging.getLogger(__name__) -class Comm(CommGeneric, AutoDB): - class DBKeys: - host = Argument() - port = Argument(1381) +class Comm(CommGeneric): + def __init__(self, dmgr, host, port=1381): + self.host = host + self.port = port def open(self): if hasattr(self, "socket"): diff --git a/artiq/coredevice/core.py b/artiq/coredevice/core.py index 2467980f5..536a10054 100644 --- a/artiq/coredevice/core.py +++ b/artiq/coredevice/core.py @@ -1,11 +1,9 @@ import os from artiq.language.core import * -from artiq.language.db import * from artiq.language.units import ns from artiq.transforms.inline import inline -from artiq.transforms.lower_units import lower_units from artiq.transforms.quantize_time import quantize_time from artiq.transforms.remove_inter_assigns import remove_inter_assigns from artiq.transforms.fold_constants import fold_constants @@ -15,7 +13,7 @@ from artiq.transforms.interleave import interleave from artiq.transforms.lower_time import lower_time from artiq.transforms.unparse import unparse -from artiq.coredevice.runtime import Environment +from artiq.coredevice.runtime import Runtime from artiq.py2llvm import get_runtime_binary @@ -47,27 +45,23 @@ def _no_debug_unparse(label, node): pass -class Core(AutoDB): - class DBKeys: - comm = Device() - ref_period = Argument(8*ns) - external_clock = Argument(False) +class Core: + def __init__(self, dmgr, ref_period=8*ns, external_clock=False): + self.comm = dmgr.get("comm") + self.ref_period = ref_period + self.external_clock = external_clock - def build(self): self.first_run = True self.core = self self.comm.core = self - self.runtime_env = Environment() + self.runtime = Runtime() def transform_stack(self, func_def, rpc_map, exception_map, debug_unparse=_no_debug_unparse): - lower_units(func_def, rpc_map) - debug_unparse("lower_units", func_def) - remove_inter_assigns(func_def) debug_unparse("remove_inter_assigns_1", func_def) - quantize_time(func_def, self.ref_period.amount) + quantize_time(func_def, self.ref_period) debug_unparse("quantize_time", func_def) fold_constants(func_def) @@ -108,7 +102,7 @@ class Core(AutoDB): debug_unparse("inline", func_def) self.transform_stack(func_def, rpc_map, exception_map, debug_unparse) - binary = get_runtime_binary(self.runtime_env, func_def) + binary = get_runtime_binary(self.runtime, func_def) return binary, rpc_map, exception_map @@ -120,15 +114,14 @@ class Core(AutoDB): binary, rpc_map, exception_map = self.compile( k_function, k_args, k_kwargs) self.comm.load(binary) - self.comm.run(k_function.__name__, self.first_run) + self.comm.run(k_function.__name__) self.comm.serve(rpc_map, exception_map) self.first_run = False @kernel - def get_rtio_time(self): - return cycles_to_time(syscall("rtio_get_counter")) + def get_rtio_counter_mu(self): + return syscall("rtio_get_counter") @kernel def break_realtime(self): - t = syscall("rtio_get_counter") + 125000 - at(cycles_to_time(t)) + at_mu(syscall("rtio_get_counter") + 125000) diff --git a/artiq/coredevice/dds.py b/artiq/coredevice/dds.py index 8ab434664..9ba4584d8 100644 --- a/artiq/coredevice/dds.py +++ b/artiq/coredevice/dds.py @@ -1,9 +1,8 @@ from artiq.language.core import * -from artiq.language.db import * from artiq.language.units import * -PHASE_MODE_DEFAULT = -1 +_PHASE_MODE_DEFAULT = -1 # keep in sync with dds.h PHASE_MODE_CONTINUOUS = 0 PHASE_MODE_ABSOLUTE = 1 @@ -23,21 +22,19 @@ class _BatchContextManager: self.dds_bus.batch_exit() -class DDSBus(AutoDB): +class DDSBus: """Core device Direct Digital Synthesis (DDS) bus batching driver. Manages batching of DDS commands on a DDS shared bus.""" - class DBKeys: - core = Device() - - def build(self): + def __init__(self, dmgr): + self.core = dmgr.get("core") self.batch = _BatchContextManager(self) @kernel def batch_enter(self): """Starts a DDS command batch. All DDS commands are buffered after this call, until ``batch_exit`` is called.""" - syscall("dds_batch_enter", time_to_cycles(now())) + syscall("dds_batch_enter", now_mu()) @kernel def batch_exit(self): @@ -46,21 +43,21 @@ class DDSBus(AutoDB): syscall("dds_batch_exit") -class DDS(AutoDB): +class _DDSGeneric: """Core device Direct Digital Synthesis (DDS) driver. Controls one DDS channel managed directly by the core device's runtime. - :param dds_sysclk: DDS system frequency, used for computing the frequency - tuning words. + This class should not be used directly, instead, use the chip-specific + drivers such as ``AD9858`` and ``AD9914``. + + :param sysclk: DDS system frequency. :param channel: channel number of the DDS device to control. """ - class DBKeys: - core = Device() - dds_sysclk = Argument(1*GHz) - channel = Argument() - - def build(self): + def __init__(self, dmgr, sysclk, channel): + self.core = dmgr.get("core") + self.sysclk = sysclk + self.channel = channel self.phase_mode = PHASE_MODE_CONTINUOUS @portable @@ -68,21 +65,33 @@ class DDS(AutoDB): """Returns the frequency tuning word corresponding to the given frequency. """ - return round(2**32*frequency/self.dds_sysclk) + return round(2**32*frequency/self.sysclk) @portable def ftw_to_frequency(self, ftw): """Returns the frequency corresponding to the given frequency tuning word. """ - return ftw*self.dds_sysclk/2**32 + return ftw*self.sysclk/2**32 + + @portable + def turns_to_pow(self, turns): + """Returns the phase offset word corresponding to the given phase + in turns.""" + return round(turns*2**self.pow_width) + + @portable + def pow_to_turns(self, pow): + """Returns the phase in turns corresponding to the given phase offset + word.""" + return pow/2**self.pow_width @kernel def init(self): """Resets and initializes the DDS channel. The runtime does this for all channels upon core device startup.""" - syscall("dds_init", time_to_cycles(now()), self.channel) + syscall("dds_init", now_mu(), self.channel) @kernel def set_phase_mode(self, phase_mode): @@ -105,17 +114,37 @@ class DDS(AutoDB): self.phase_mode = phase_mode @kernel - def set(self, frequency, phase_mode=PHASE_MODE_DEFAULT, phase_offset=0): + def set_mu(self, frequency, phase=0, phase_mode=_PHASE_MODE_DEFAULT): """Sets the DDS channel to the specified frequency and phase. + This uses machine units (FTW and POW). The frequency tuning word width + is 32, whereas the phase offset word width depends on the type of DDS + chip and can be retrieved via the ``pow_width`` attribute. + :param frequency: frequency to generate. + :param phase: adds an offset, in turns, to the phase. :param phase_mode: if specified, overrides the default phase mode set by ``set_phase_mode`` for this call. - :param phase_offset: adds an offset, in turns, to the phase. """ - if phase_mode == PHASE_MODE_DEFAULT: + if phase_mode == _PHASE_MODE_DEFAULT: phase_mode = self.phase_mode + syscall("dds_set", now_mu(), self.channel, + frequency, round(phase*2**self.pow_width), phase_mode) - syscall("dds_set", time_to_cycles(now()), self.channel, - self.frequency_to_ftw(frequency), round(phase_offset*2**14), - self.phase_mode) + @kernel + def set(self, frequency, phase=0, phase_mode=_PHASE_MODE_DEFAULT): + """Like ``set_mu``, but uses Hz and turns.""" + self.set_mu(self.frequency_to_ftw(frequency), + self.turns_to_pow(phase), phase_mode) + + +class AD9858(_DDSGeneric): + """Driver for AD9858 DDS chips. See ``_DDSGeneric`` for a description + of the functionality.""" + pow_width = 14 + + +class AD9914(_DDSGeneric): + """Driver for AD9914 DDS chips. See ``_DDSGeneric`` for a description + of the functionality.""" + pow_width = 16 diff --git a/artiq/coredevice/runtime.py b/artiq/coredevice/runtime.py index 1fff8f4fc..84a6bb386 100644 --- a/artiq/coredevice/runtime.py +++ b/artiq/coredevice/runtime.py @@ -1,7 +1,7 @@ import os -import llvmlite.ir as ll -import llvmlite.binding as llvm +import llvmlite_or1k.ir as ll +import llvmlite_or1k.binding as llvm from artiq.py2llvm import base_types, fractions, lists from artiq.language import units @@ -21,6 +21,7 @@ _syscalls = { "ttl_set_oe": "Iib:n", "ttl_set_sensitivity": "Iii:n", "ttl_get": "iI:I", + "ttl_clock_set": "Iii:n", "dds_init": "Ii:n", "dds_batch_enter": "I:n", "dds_batch_exit": "n:n", @@ -195,7 +196,7 @@ def _debug_dump_obj(obj): raise IOError -class Environment(LinkInterface): +class Runtime(LinkInterface): def __init__(self): self.cpu_type = "or1k" # allow 1ms for all initial DDS programming @@ -208,4 +209,4 @@ class Environment(LinkInterface): return obj def __repr__(self): - return "".format(self.cpu_type) + return "".format(self.cpu_type) diff --git a/artiq/coredevice/ttl.py b/artiq/coredevice/ttl.py index 359b9b590..644e55ff6 100644 --- a/artiq/coredevice/ttl.py +++ b/artiq/coredevice/ttl.py @@ -1,49 +1,7 @@ from artiq.language.core import * -from artiq.language.db import * -class LLTTLOut(AutoDB): - """Low-level RTIO TTL output driver. - - Allows setting RTIO TTL outputs at arbitrary times, without time - unit conversion. - - This is meant to be used mostly in drivers; consider using - ``TTLOut`` instead. - - This should be used with output-only channels. - """ - class DBKeys: - core = Device() - channel = Argument() - - @kernel - def set_o(self, t, value): - """Sets the output value of the RTIO channel. - - :param t: timestamp in RTIO cycles (64-bit integer). - :param value: value to set at the output. - """ - syscall("ttl_set_o", t, self.channel, value) - - @kernel - def on(self, t): - """Turns the RTIO channel on. - - :param t: timestamp in RTIO cycles (64-bit integer). - """ - self.set_o(t, True) - - @kernel - def off(self, t): - """Turns the RTIO channel off. - - :param t: timestamp in RTIO cycles (64-bit integer). - """ - self.set_o(t, False) - - -class TTLOut(AutoDB): +class TTLOut: """RTIO TTL output driver. This should be used with output-only channels. @@ -51,45 +9,53 @@ class TTLOut(AutoDB): :param core: core device :param channel: channel number """ - class DBKeys: - core = Device() - channel = Argument() + def __init__(self, dmgr, channel): + self.core = dmgr.get("core") + self.channel = channel - - def build(self): # in RTIO cycles self.o_previous_timestamp = int64(0) @kernel - def _set_o(self, o): - syscall("ttl_set_o", time_to_cycles(now()), self.channel, o) - self.o_previous_timestamp = time_to_cycles(now()) + def set_o(self, o): + syscall("ttl_set_o", now_mu(), self.channel, o) + self.o_previous_timestamp = now_mu() @kernel def sync(self): - """Busy-waits until all programmed level switches have been effected.""" + """Busy-wait until all programmed level switches have been + effected.""" while syscall("rtio_get_counter") < self.o_previous_timestamp: pass @kernel def on(self): """Sets the output to a logic high state.""" - self._set_o(True) + self.set_o(True) @kernel def off(self): - """Sets the output to a logic low state.""" - self._set_o(False) + """Set the output to a logic low state.""" + self.set_o(False) + + @kernel + def pulse_mu(self, duration): + """Pulse the output high for the specified duration + (in machine units).""" + self.on() + delay_mu(duration) + self.off() @kernel def pulse(self, duration): - """Pulses the output high for the specified duration.""" + """Pulse the output high for the specified duration + (in seconds).""" self.on() delay(duration) self.off() -class TTLInOut(AutoDB): +class TTLInOut: """RTIO TTL input/output driver. In output mode, provides functions to set the logic level on the signal. @@ -107,78 +73,113 @@ class TTLInOut(AutoDB): :param core: core device :param channel: channel number """ - class DBKeys: - core = Device() - channel = Argument() + def __init__(self, dmgr, channel): + self.core = dmgr.get("core") + self.channel = channel - def build(self): # in RTIO cycles self.o_previous_timestamp = int64(0) self.i_previous_timestamp = int64(0) @kernel - def _set_oe(self, oe): - syscall("ttl_set_oe", time_to_cycles(now()), self.channel, oe) + def set_oe(self, oe): + syscall("ttl_set_oe", now_mu(), self.channel, oe) @kernel def output(self): - self._set_oe(True) + self.set_oe(True) @kernel def input(self): - self._set_oe(False) + self.set_oe(False) @kernel - def _set_o(self, o): - syscall("ttl_set_o", time_to_cycles(now()), self.channel, o) - self.o_previous_timestamp = time_to_cycles(now()) + def set_o(self, o): + syscall("ttl_set_o", now_mu(), self.channel, o) + self.o_previous_timestamp = now_mu() @kernel def sync(self): - """Busy-waits until all programmed level switches have been effected.""" + """Busy-wait until all programmed level switches have been + effected.""" while syscall("rtio_get_counter") < self.o_previous_timestamp: pass @kernel def on(self): - """Sets the output to a logic high state.""" - self._set_o(True) + """Set the output to a logic high state.""" + self.set_o(True) @kernel def off(self): - """Sets the output to a logic low state.""" - self._set_o(False) + """Set the output to a logic low state.""" + self.set_o(False) + + @kernel + def pulse_mu(self, duration): + """Pulses the output high for the specified duration + (in machine units).""" + self.on() + delay_mu(duration) + self.off() @kernel def pulse(self, duration): - """Pulses the output high for the specified duration.""" + """Pulses the output high for the specified duration + (in seconds).""" self.on() delay(duration) self.off() @kernel def _set_sensitivity(self, value): - syscall("ttl_set_sensitivity", time_to_cycles(now()), self.channel, value) - self.i_previous_timestamp = time_to_cycles(now()) + syscall("ttl_set_sensitivity", now_mu(), self.channel, value) + self.i_previous_timestamp = now_mu() + + @kernel + def gate_rising_mu(self, duration): + """Register rising edge events for the specified duration + (in machine units).""" + self._set_sensitivity(1) + delay_mu(duration) + self._set_sensitivity(0) + + @kernel + def gate_falling_mu(self, duration): + """Register falling edge events for the specified duration + (in machine units).""" + self._set_sensitivity(2) + delay_mu(duration) + self._set_sensitivity(0) + + @kernel + def gate_both_mu(self, duration): + """Register both rising and falling edge events for the specified + duration (in machine units).""" + self._set_sensitivity(3) + delay_mu(duration) + self._set_sensitivity(0) @kernel def gate_rising(self, duration): - """Register rising edge events for the specified duration.""" + """Register rising edge events for the specified duration + (in seconds).""" self._set_sensitivity(1) delay(duration) self._set_sensitivity(0) @kernel def gate_falling(self, duration): - """Register falling edge events for the specified duration.""" + """Register falling edge events for the specified duration + (in seconds).""" self._set_sensitivity(2) delay(duration) self._set_sensitivity(0) @kernel - def gate_both(self, duration): + def gate_both_mu(self, duration): """Register both rising and falling edge events for the specified - duration.""" + duration (in seconds).""" self._set_sensitivity(3) delay(duration) self._set_sensitivity(0) @@ -194,11 +195,81 @@ class TTLInOut(AutoDB): return count @kernel - def timestamp(self): + def timestamp_mu(self): """Poll the RTIO input and returns an event timestamp, according to the gating. If the gate is permanently closed, returns a negative value. """ - return cycles_to_time(syscall("ttl_get", self.channel, - self.i_previous_timestamp)) + return syscall("ttl_get", self.channel, self.i_previous_timestamp) + + +class TTLClockGen: + """RTIO TTL clock generator driver. + + This should be used with TTL channels that have a clock generator + built into the gateware (not compatible with regular TTL channels). + + :param core: core device + :param channel: channel number + """ + def __init__(self, dmgr, channel): + self.core = dmgr.get("core") + self.channel = channel + + def build(self): + # in RTIO cycles + self.previous_timestamp = int64(0) + self.acc_width = 24 + + @portable + def frequency_to_ftw(self, frequency): + """Returns the frequency tuning word corresponding to the given + frequency. + """ + return round(2**self.acc_width*frequency*self.core.ref_period) + + @portable + def ftw_to_frequency(self, ftw): + """Returns the frequency corresponding to the given frequency tuning + word. + """ + return ftw/self.core.ref_period/2**self.acc_width + + @kernel + def set_mu(self, frequency): + """Set the frequency of the clock, in machine units. + + This also sets the phase, as the time of the first generated rising + edge corresponds to the time of the call. + + The clock generator contains a 24-bit phase accumulator operating on + the RTIO clock. At each RTIO clock tick, the frequency tuning word is + added to the phase accumulator. The most significant bit of the phase + accumulator is connected to the TTL line. Setting the frequency tuning + word has the additional effect of setting the phase accumulator to + 0x800000. + + Due to the way the clock generator operates, frequency tuning words + that are not powers of two cause jitter of one RTIO clock cycle at the + output. + """ + syscall("ttl_clock_set", now_mu(), self.channel, frequency) + self.previous_timestamp = now_mu() + + @kernel + def set(self, frequency): + """Like ``set_mu``, but using Hz.""" + self.set_mu(self.frequency_to_ftw(frequency)) + + @kernel + def stop(self): + """Stop the toggling of the clock and set the output level to 0.""" + self.set_mu(0) + + @kernel + def sync(self): + """Busy-wait until all programmed frequency switches and stops have + been effected.""" + while syscall("rtio_get_counter") < self.o_previous_timestamp: + pass diff --git a/artiq/devices/lda/driver.py b/artiq/devices/lda/driver.py index 3f5aaa2e2..d84605201 100644 --- a/artiq/devices/lda/driver.py +++ b/artiq/devices/lda/driver.py @@ -2,7 +2,7 @@ import logging import ctypes import struct -from artiq.language.units import dB, check_unit, Quantity +from artiq.language.units import dB logger = logging.getLogger("lda") @@ -47,14 +47,7 @@ class Ldasim: """ step = self.get_att_step_size() - - if isinstance(attenuation, Quantity): - check_unit(attenuation, "dB") - att = attenuation - else: - att = attenuation*dB - - att = round(att/step)*step + att = round(attenuation/step)*step if att > self.get_att_max(): raise ValueError("Cannot set attenuation {} > {}" @@ -62,7 +55,7 @@ class Ldasim: elif att < 0*dB: raise ValueError("Cannot set attenuation {} < 0".format(att)) else: - att = round(att.amount*4)/4. * dB + att = round(att*4)/4. * dB self._attenuation = att def ping(self): @@ -117,7 +110,7 @@ class Lda: self._product_ids[self.product], self.serial) if not self._dev: - raise IOError + raise IOError("Device not found") def close(self): """Close the device.""" @@ -218,14 +211,7 @@ class Lda: """ step = self.get_att_step_size() - - if isinstance(attenuation, Quantity): - check_unit(attenuation, "dB") - att = attenuation - else: - att = attenuation*dB - - att = round(att/step)*step + att = round(attenuation/step)*step if att > self.get_att_max(): raise ValueError("Cannot set attenuation {} > {}" @@ -233,7 +219,7 @@ class Lda: elif att < 0*dB: raise ValueError("Cannot set attenuation {} < 0".format(att)) else: - self.set(0x8d, bytes([int(round(att.amount*4))])) + self.set(0x8d, bytes([int(round(att*4))])) def ping(self): try: diff --git a/artiq/devices/novatech409b/driver.py b/artiq/devices/novatech409b/driver.py index 945b959a3..5bea7164e 100644 --- a/artiq/devices/novatech409b/driver.py +++ b/artiq/devices/novatech409b/driver.py @@ -15,10 +15,26 @@ class UnexpectedResponse(Exception): class Novatech409B: - """Driver for Novatech 409B 4-channel DDS""" + """Driver for Novatech 409B 4-channel DDS. - # maximum frequency of Novatech 409B when using PLL and external reference - max_freq_with_pll = 171.1276031 + All output channels are in range [0, 1, 2, 3]. + All frequencies are in Hz. + All phases are in turns. + All amplitudes are in volts. + """ + + error_codes = { + "?0": "Unrecognized Command", + "?1": "Bad Frequency", + "?2": "Bad AM Command", + "?3": "Input line too long", + "?4": "Bad Phase", + "?5": "Bad Time", + "?6": "Bad Mode", + "?7": "Bad Amp", + "?8": "Bad Constant", + "?f": "Bad Byte" + } def __init__(self, serial_dev): if serial_dev is None: @@ -32,68 +48,67 @@ class Novatech409B: parity="N", stopbits=1, xonxoff=0, - timeout=0.2) + timeout=1.0) self.setup() def close(self): - """Close the serial port""" + """Close the serial port.""" if not self.simulation: self.port.close() def _ser_send(self, cmd, get_response=True): - """send a string to the serial port + """Send a string to the serial port.""" - Routine for sending serial commands to device. It sends strings - and listens for a response terminated by a carriage return. + # Low-level routine for sending serial commands to device. It sends + # strings and listens for a response terminated by a carriage return. + # example: + # ser_send("F0 1.0") # sets the freq of channel 0 to 1.0 MHz - example: - ser_send("F0 1.0") # sets the freq of channel 0 to 1.0 MHz - - :param cmd: a character string to send to device - :returns: None - """ if self.simulation: print(cmd) else: - self.port.flush() + self.port.flushInput() self.port.write((cmd + "\r\n").encode()) + result = self.port.readline().rstrip().decode() if get_response: - result = self.port.readline().rstrip().decode() - if result != "OK": - raise UnexpectedResponse(result) + logger.debug("got response from device: %s", result) + if result == "OK": + pass + elif result == "": + raise UnexpectedResponse("Response from device timed out") + else: + try: + errstr = self.error_codes[result] + except KeyError: + errstr = "Unrecognized reply: '{}'".format(result) + s = "Error Code = {ec}, {ecs}".format(ec=result, ecs=errstr) + raise UnexpectedResponse(s) + else: + pass def reset(self): - """command hardware reset of 409B - - returns: None - """ + """Hardware reset of 409B.""" self._ser_send("R", get_response=False) time.sleep(1) self.setup() def setup(self): - """initial setup of 409B + """Initial setup of 409B.""" - Setup the Novatech 409B with the following defaults. - * command echo off ("E d") - * external clock ("") 10 MHz sinusoid -1 to +7 dBm + # Setup the Novatech 409B with the following defaults: + # * command echo off ("E d") + # * external clock ("") 10 MHz sinusoid -1 to +7 dBm - :returns: None - """ - # disable command echo self._ser_send("E d", get_response=False) self.set_phase_continuous(True) self.set_simultaneous_update(False) def save_state_to_eeprom(self): - """save current state to EEPROM - - Saves current state into EEPROM and sets valid flag. - State used as default upon next power up or reset. """ + """Save current state to EEPROM.""" self._ser_send("S") def set_phase_continuous(self, is_continuous): - """toggle phase continuous mode + """Toggle phase continuous mode. Sends the "M n" command. This turns off the automatic clearing of the phase register. In this mode, the phase @@ -109,7 +124,9 @@ class Novatech409B: self._ser_send("M a") def set_simultaneous_update(self, simultaneous): - """Sends the "I m" command. In this mode an update + """Set simultaneous update mode. + + Sends the "I m" command. In this mode an update pulse will not be sent to the DDS chip until an "I p" command is sent. This is useful when it is important to change all the outputs to new values @@ -121,140 +138,75 @@ class Novatech409B: self._ser_send("I a") def set_freq(self, ch_no, freq): - """set_freq(ch_no,freq): - Set ch_no to frequency freq MHz""" - if ch_no < 0 or ch_no > 3: - raise ValueError("Incorrect channel number {}".format(ch_no)) - if freq < 0.0 or freq > self.max_freq_with_pll: - raise ValueError("Incorrect frequency {}".format(freq)) - # do this immediately, disable SimultaneousUpdate mode + """Set frequency of one channel.""" self.set_simultaneous_update(False) - self._ser_send("F{:d} {:f}".format(ch_no, freq)) + # Novatech expects MHz + self._ser_send("F{:d} {:f}".format(ch_no, freq/1e6)) def set_phase(self, ch_no, phase): - """set DDS phase - - :param ch_no: 0 to 3 - :param phase: phase angle in cycles [0, 1] - :returns: None - """ - if ch_no < 0 or ch_no > 3: - raise ValueError("Incorrect channel number {}".format(ch_no)) - if phase < 0 or phase > 1: - raise ValueError("Incorrect phase {}".format(phase)) + """Set phase of one channel.""" # do this immediately, disable SimultaneousUpdate mode self.set_simultaneous_update(False) # phase word is required by device # N is an integer from 0 to 16383. Phase is set to # N*360/16384 deg; in ARTIQ represent phase in cycles [0, 1] - phase_word = round(phase*16384) - if phase_word >= 16384: - phase_word -= 16384 + phase_word = round(phase*16383) cmd = "P{:d} {:d}".format(ch_no, phase_word) self._ser_send(cmd) def set_freq_all_phase_continuous(self, freq): - """set frequency of all channels simultaneously + """Set frequency of all channels simultaneously. Set frequency of all channels simultaneously. 1) all DDSs are set to phase continuous mode 2) all DDSs are simultaneously set to new frequency Together 1 and 2 ensure phase continuous frequency switching. - - :param freq: frequency in MHz - :returns: None """ self.set_simultaneous_update(True) self.set_phase_continuous(True) - for channel_num in range(4): - self.set_freq(channel_num, freq) + for i in range(4): + self.set_freq(i, freq) # send command necessary to update all channels at the same time self._ser_send("I p") def set_phase_all(self, phase): - """set phase of all DDS channels simultaneously + """Set phase of all channels simultaneously.""" - Set phase of all DDS channels at the same time. For example,:: - set_phase_all([0, .25, 0.5, 0.75]) - - :param phase: vector of four phases (in cycles [0, 1]) - :returns: None - """ self.set_simultaneous_update(True) # Note that this only works if the continuous # phase switching is turned off. self.set_phase_continuous(False) - for ch_no in range(4): - self.set_phase(ch_no, phase[ch_no]) + for i in range(4): + self.set_phase(i, phase) # send command necessary to update all channels at the same time self._ser_send("I p") - def freq_sweep_all_phase_continuous(self, f0, f1, t): - """ sweep phase of all DDSs, phase continuous + def set_gain(self, ch_no, volts): + """Set amplitude of one channel.""" - Sweep frequency in a phase continuous fashion. + # due to error in Novatech it doesn't generate an error for + # dac_value>1024, so need to trap. + dac_value = int(math.floor(volts/0.51*1024)) + if dac_value < 0 or dac_value > 1023: + s = "Amplitude out of range {v}".format(v=volts) + raise ValueError(s) - :param f0: starting frequency (MHz) - :param f1: ending frequency (MHz) - :param t: sweep duration (seconds) - :returns: None - """ - # TODO: consider using artiq.language.units - if f0 == f1: - return - # get sign of sweep - if f1 > f0: - df_sign = 1 - else: - df_sign = -1 - - self.set_phase_continuous(True) - self.set_simultaneous_update(True) - # calculate delay - # note that a single call to self.set_freq_all_phase_continuous() - # takes time t_for_one_freq_set; fix duration empirically - t_for_one_freq_set = 0.264 - dt = t_for_one_freq_set - n_steps = int(math.ceil(t/dt)) - df = abs(f0-f1)/n_steps - for n in range(n_steps): - fnow = f0+n*df_sign*df - self.set_freq_all_phase_continuous(fnow) - self.set_freq_all_phase_continuous(f1) - - def output_scale(self, ch_no, frac): - """changes amplitude of a DDS - - :param ch_no: DDS channel 0, 1, 2 or 3 - :param frac: 0 to 1 (full attenuation to no attenuation) - :returns: None - """ self.set_simultaneous_update(False) - dac_ch_no = int(math.floor(frac*1024)) - s = "V{:d} {:d}".format(ch_no, dac_ch_no) + s = "V{:d} {:d}".format(ch_no, dac_value) self._ser_send(s) - def output_scale_all(self, frac): - """changes amplitude of all DDSs - - :param frac: 0 to 1 (full attenuation to no attenuation) - """ - for ch_no in range(4): - self.output_scale(ch_no, frac) - - def output_on_off(self, ch_no, on): - """turns on or off the DDS - - :param ch_no: DDS channel 0, 1, 2 or 3 - """ - if on: - self.output_scale(ch_no, 1.0) + def get_status(self): + if self.simulation: + return ["00989680 2000 01F5 0000 00000000 00000000 000301", + "00989680 2000 01F5 0000 00000000 00000000 000301", + "00989680 2000 01F5 0000 00000000 00000000 000301", + "00989680 2000 01F5 0000 00000000 00000000 000301", + "80 BC0000 0000 0102 21"] else: - self.output_scale(ch_no, 0.0) - - def output_on_off_all(self, on): - """turns on or off the all the DDSs""" - if on: - self.output_scale_all(1.0) - else: - self.output_scale_all(0.0) + # status message is multi-line + self.port.flushInput() + self.port.write(("QUE" + "\r\n").encode()) + result = self.port.readlines() + result = [r.rstrip().decode() for r in result] + logger.debug("got device status: %s", result) + return result diff --git a/artiq/devices/pdq2/mediator.py b/artiq/devices/pdq2/mediator.py index 63d3d7b72..27265bf9e 100644 --- a/artiq/devices/pdq2/mediator.py +++ b/artiq/devices/pdq2/mediator.py @@ -1,7 +1,5 @@ from artiq.language.core import * -from artiq.language.db import * from artiq.language.units import * -from artiq.coredevice import ttl frame_setup = 20*ns @@ -76,7 +74,7 @@ class _Frame: self.pdq = pdq self.frame_number = frame_number self.segments = [] - self.segment_count = 0 + self.segment_count = 0 # == len(self.segments), used in kernel self.invalidated = False @@ -99,7 +97,7 @@ class _Frame: def _arm(self): self.segment_delays = [ - time_to_cycles(s.get_duration()*delay_margin_factor, self.core) + seconds_to_mu(s.get_duration()*delay_margin_factor, self.core) for s in self.segments] def _invalidate(self): @@ -125,7 +123,8 @@ class _Frame: if not self.pdq.armed: raise ArmError - t = time_to_cycles(now()) - time_to_cycles(trigger_duration/2) + call_t = now_mu() + trigger_start_t = call_t - seconds_to_mu(trigger_duration/2) if self.pdq.current_frame >= 0: # PDQ is in the middle of a frame. Check it is us. @@ -136,15 +135,16 @@ class _Frame: # to play our first segment. self.pdq.current_frame = self.frame_number self.pdq.next_segment = 0 - t2 = t - time_to_cycles(frame_setup) - self.pdq.frame0.set_value(t2, self.frame_number & 1) - self.pdq.frame1.set_value(t2, (self.frame_number & 2) >> 1) - self.pdq.frame2.set_value(t2, (self.frame_number & 4) >> 2) + at_mu(trigger_start_t - 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.trigger.on(t) - self.pdq.trigger.off(t + time_to_cycles(trigger_duration)) + at_mu(trigger_start_t) + self.pdq.trigger.pulse(trigger_duration) - delay(cycles_to_time(self.segment_delays[self.pdq.next_segment])) + at_mu(call_t) + delay_mu(self.segment_delays[self.pdq.next_segment]) self.pdq.next_segment += 1 # test for end of frame @@ -153,23 +153,14 @@ class _Frame: self.pdq.next_segment = -1 -class CompoundPDQ2(AutoDB): - class DBKeys: - core = Device() - pdq2_devices = Argument() - rtio_trigger = Argument() - rtio_frame = Argument() - - def build(self): - self.pdq2s = [self.dbh.get_device(d) for d in self.pdq2_devices] - self.trigger = ttl.LLTTLOut( - core=self.core, channel=self.rtio_trigger) - self.frame0 = ttl.LLTTLOut( - core=self.core, channel=self.rtio_frame[0]) - self.frame1 = ttl.LLTTLOut( - core=self.core, channel=self.rtio_frame[1]) - self.frame2 = ttl.LLTTLOut( - core=self.core, channel=self.rtio_frame[2]) +class CompoundPDQ2: + def __init__(self, dmgr, pdq2_devices, trigger_device, frame_devices): + self.core = dmgr.get("core") + self.pdq2s = [dmgr.get(d) for d in self.pdq2_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.frames = [] self.current_frame = -1 @@ -187,6 +178,7 @@ class CompoundPDQ2(AutoDB): raise ArmError for frame in self.frames: frame._arm() + self.armed = True full_program = [f._get_program() for f in self.frames] for n, pdq2 in enumerate(self.pdq2s): diff --git a/artiq/devices/pxi6733/driver.py b/artiq/devices/pxi6733/driver.py index 118169ec2..22839ee04 100644 --- a/artiq/devices/pxi6733/driver.py +++ b/artiq/devices/pxi6733/driver.py @@ -1,9 +1,14 @@ # Yann Sionneau , 2015 -from ctypes import byref +from ctypes import byref, c_ulong +import logging + import numpy as np +logger = logging.getLogger(__name__) + + class DAQmxSim: def load_sample_values(self, values): pass @@ -11,61 +16,124 @@ class DAQmxSim: def close(self): pass + def ping(self): + return True + class DAQmx: """NI PXI6733 DAQ interface.""" - def __init__(self, device, analog_output, clock): - import PyDAQmx as daq + def __init__(self, channels, clock): + """ + :param channels: List of channels as a string, following + the physical channels lists and ranges NI-DAQmx syntax. - self.device = device - self.analog_output = analog_output - self.clock = clock - self.tasks = [] - self.daq = daq + Example: Dev1/ao0, Dev1/ao1:ao3 + :param clock: Clock source terminal as a string, following + NI-DAQmx terminal names syntax. - def done_callback_py(self, taskhandle, status, callback_data): - self.daq.DAQmxClearTask(taskhandle) - self.tasks.remove(taskhandle) - - def load_sample_values(self, values): - """Load sample values into PXI 6733 device. - - This loads sample values into the PXI 6733 device and then - configures a task to output those samples at each clock rising - edge. - - A callback is registered to clear the task (deallocate resources) - when the task has completed. - - :param values: A numpy array of sample values to load in the device. + Example: PFI5 """ + import PyDAQmx as daq + + self.channels = channels.encode() + self.clock = clock.encode() + self.task = None + self.daq = daq + + def _done_callback(self, taskhandle, status, callback_data): + if taskhandle != self.task: + logger.warning("done callback called with unexpected task") + else: + self.clear_pending_task() + + def ping(self): + try: + data = (c_ulong*1)() + self.daq.DAQmxGetDevSerialNum(self.device, data) + except: + return False + return True + + def load_sample_values(self, sampling_freq, values): + """Load sample values into PXI 6733 device. + + This loads sample values into the PXI 6733 device. + The device will output samples at each clock rising edge. + The device waits for a clock rising edge to output the first sample. + + When using several channels simultaneously, you can either concatenate + the values for the different channels in a 1-dimensional ``values`` + numpy ndarray. + + Example: + + >>> values = np.array([ch0_samp0, ch0_samp1, ch1_samp0, ch1_samp1], + dtype=float) + + In this example the first two samples will be output via the first + channel and the two following samples will be output via the second + channel. + + Or you can use a 2-dimensional numpy ndarray like this: + + >>> values = np.array([[ch0_samp0, ch0_samp1],[ch1_samp0, ch1_samp1]], + dtype=float) + + Any call to this method will cancel any previous task even if it has + not yet completed. + + :param sampling_freq: The sampling frequency in samples per second. + :param values: A numpy ndarray of sample values (in volts) to load in + the device. + """ + + self.clear_pending_task() + values = values.flatten() t = self.daq.Task() - t.CreateAOVoltageChan(self.device+b"/"+self.analog_output, b"", + t.CreateAOVoltageChan(self.channels, b"", min(values), max(values), self.daq.DAQmx_Val_Volts, None) - t.CfgSampClkTiming(self.clock, 1000.0, self.daq.DAQmx_Val_Rising, - self.daq.DAQmx_Val_FiniteSamps, len(values)) + + channel_number = (c_ulong*1)() + t.GetTaskNumChans(channel_number) + nb_values = len(values) + if nb_values % channel_number[0]: + self.daq.DAQmxClearTask(t.taskHandle) + raise ValueError("The size of the values array must be a multiple " + "of the number of channels ({})" + .format(channel_number[0])) + samps_per_channel = nb_values // channel_number[0] + + t.CfgSampClkTiming(self.clock, sampling_freq, + self.daq.DAQmx_Val_Rising, + self.daq.DAQmx_Val_FiniteSamps, samps_per_channel) num_samps_written = self.daq.int32() values = np.require(values, dtype=float, requirements=["C_CONTIGUOUS", "WRITEABLE"]) - ret = t.WriteAnalogF64(len(values), False, 0, + ret = t.WriteAnalogF64(samps_per_channel, False, 0, self.daq.DAQmx_Val_GroupByChannel, values, byref(num_samps_written), None) - if num_samps_written.value != len(values): + if num_samps_written.value != nb_values: raise IOError("Error: only {} sample values were written" .format(num_samps_written.value)) if ret: raise IOError("Error while writing samples to the channel buffer") - done_cb = self.daq.DAQmxDoneEventCallbackPtr(self.done_callback_py) - self.tasks.append(t.taskHandle) + done_cb = self.daq.DAQmxDoneEventCallbackPtr(self._done_callback) + self.task = t.taskHandle self.daq.DAQmxRegisterDoneEvent(t.taskHandle, 0, done_cb, None) t.StartTask() - def close(self): - """Clear all pending tasks.""" + def clear_pending_task(self): + """Clear any pending task.""" - for t in self.tasks: - self.daq.DAQmxClearTask(t) + if self.task is not None: + self.daq.DAQmxClearTask(self.task) + self.task = None + + def close(self): + """Free any allocated resources.""" + + self.clear_pending_task() diff --git a/artiq/devices/pxi6733/mediator.py b/artiq/devices/pxi6733/mediator.py new file mode 100644 index 000000000..047653b61 --- /dev/null +++ b/artiq/devices/pxi6733/mediator.py @@ -0,0 +1,177 @@ +import numpy as np + +from artiq.language.core import * +from artiq.language.units import * +from artiq.wavesynth.compute_samples import Synthesizer + + +class SegmentSequenceError(Exception): + """Raised when attempting to play back a named segment which is not the + next in the sequence.""" + pass + + +class InvalidatedError(Exception): + """Raised when attemting to use a frame or segment that has been + invalidated (due to disarming the DAQmx).""" + pass + + +class ArmError(Exception): + """Raised when attempting to arm an already armed DAQmx, to modify the + program of an armed DAQmx, or to play a segment on a disarmed DAQmx.""" + pass + + +def _ceil_div(a, b): + return (a + b - 1)//b + + +def _compute_duration_mu(nsamples, ftw, acc_width): + # This returns the precise duration so that the clock can be stopped + # exactly at the next rising edge (RTLink commands take precedence over + # toggling from the accumulator). + # If segments are played continuously, replacement of the stop command + # will keep the clock running. If the FTW is not a power of two, note that + # the accumulator is reset at that time, which causes jitter and frequency + # inaccuracy. + # Formally: + # duration *ftw >= nsamples*2**acc_width + # (duration - 1)*ftw < nsamples*2**acc_width + return _ceil_div(nsamples*2**acc_width, ftw) + + +class _Segment: + def __init__(self, frame, segment_number): + self.frame = frame + self.segment_number = segment_number + + self.lines = [] + + # for @kernel + self.core = frame.daqmx.core + + def add_line(self, duration, channel_data): + if self.frame.invalidated: + raise InvalidatedError + if self.frame.daqmx.armed: + raise ArmError + self.lines.append((duration, channel_data)) + + @kernel + def advance(self): + if self.frame.invalidated: + raise InvalidatedError + if not self.frame.daqmx.armed: + raise ArmError + # If the frame is currently being played, check that we are next. + if (self.frame.daqmx.next_segment >= 0 + and self.frame.daqmx.next_segment != self.segment_number): + raise SegmentSequenceError + self.frame.advance() + + +class _Frame: + def __init__(self, daqmx): + self.daqmx = daqmx + self.segments = [] + self.segment_count = 0 # == len(self.segments), used in kernel + + self.invalidated = False + + # for @kernel + self.core = self.daqmx.core + + def create_segment(self, name=None): + if self.invalidated: + raise InvalidatedError + if self.daqmx.armed: + raise ArmError + segment = _Segment(self, self.segment_count) + if name is not None: + if hasattr(self, name): + raise NameError("Segment name already exists") + setattr(self, name, segment) + self.segments.append(segment) + self.segment_count += 1 + return segment + + def _arm(self): + self.segment_delays = [ + _compute_duration_mu(s.get_sample_count(), + self.daqmx.sample_rate, + self.daqmx.clock.acc_width) + for s in self.segments] + + def _invalidate(self): + self.invalidated = True + + def _get_samples(self): + program = [ + { + "dac_divider": 1, + "duration": duration, + "channel_data": channel_data, + } for duration, channel_data in segment.lines + for segment in self.segments] + synth = Synthesizer(self.daqmx.channel_count, program) + synth.select(0) + # not setting any trigger flag in the program causes the whole + # waveform to be computed here for all segments. + # slicing the segments is done by stopping the clock. + return synth.trigger() + + @kernel + def advance(self): + if self.invalidated: + raise InvalidatedError + if not self.daqmx.armed: + raise ArmError + + self.daqmx.clock.set(self.daqmx.sample_rate) + delay_mu(self.segment_delays[self.daqmx.next_segment]) + self.daqmx.next_segment += 1 + self.daqmx.clock.stop() + + # test for end of frame + if self.daqmx.next_segment == self.segment_count: + self.daqmx.next_segment = -1 + + +class CompoundDAQmx: + def __init__(self, dmgr, daqmx_device, clock_device, channel_count, + sample_rate, sample_rate_in_mu=False): + self.core = dmgr.get("core") + self.daqmx = dmgr.get(daqmx_device) + self.clock = dmgr.get(clock_device) + self.channel_count = channel_count + if self.sample_rate_in_mu: + self.sample_rate = sample_rate + else: + self.sample_rate = self.clock.frequency_to_ftw(sample_rate) + + self.frame = None + self.next_segment = -1 + self.armed = False + + def disarm(self): + if self.frame is not None: + self.frame._invalidate() + self.frame = None + self.armed = False + + def arm(self): + if self.armed: + raise ArmError + if self.frame is not None: + self.frame._arm() + self.daqmx.load_sample_values( + self.clock.ftw_to_frequency(self.sample_rate), + np.array(self.frame._get_samples())) + self.armed = True + + def create_frame(self): + if self.armed: + raise ArmError + self.frame = _Frame(self) + return self.frame diff --git a/artiq/devices/thorlabs_tcube/driver.py b/artiq/devices/thorlabs_tcube/driver.py index 649b051c7..e7e759d5a 100644 --- a/artiq/devices/thorlabs_tcube/driver.py +++ b/artiq/devices/thorlabs_tcube/driver.py @@ -4,7 +4,7 @@ import struct as st import serial -from artiq.language.units import V, strip_unit +from artiq.language.units import V logger = logging.getLogger(__name__) @@ -221,10 +221,10 @@ class Tcube: def handle_message(self, msg): pass - def send_request(self, msgreq_id, msgget_id, param1=0, param2=0): - Message(msgreq_id, param1, param2).send(self.port) + def send_request(self, msgreq_id, wait_for_msgs, param1=0, param2=0, data=None): + Message(msgreq_id, param1, param2, data=data).send(self.port) msg = msg_id = None - while msg is None or msg_id != msgget_id: + while msg is None or msg_id not in wait_for_msgs: msg = Message.recv(self.port) self.handle_message(msg) msg_id = msg.id @@ -247,7 +247,7 @@ class Tcube: def get_channel_enable_state(self): get_msg = self.send_request(MGMSG.MOD_REQ_CHANENABLESTATE, - MGMSG.MOD_GET_CHANENABLESTATE, 1) + [MGMSG.MOD_GET_CHANENABLESTATE], 1) self.chan_enabled = get_msg.param2 if self.chan_enabled == 1: self.chan_enabled = True @@ -286,7 +286,7 @@ class Tcube: def hardware_request_information(self): return self.send_request(MGMSG.HW_REQ_INFO, - MGMSG.HW_GET_INFO) + [MGMSG.HW_GET_INFO]) def is_channel_enabled(self): return self.chan_enabled @@ -302,7 +302,7 @@ class Tcube: class Tpz(Tcube): def __init__(self, serial_dev): Tcube.__init__(self, serial_dev) - self.voltage_limit = self.get_tpz_io_settings()[0].amount + self.voltage_limit = self.get_tpz_io_settings()[0] def handle_message(self, msg): msg_id = msg.id @@ -355,7 +355,7 @@ class Tpz(Tcube): """ get_msg = self.send_request(MGMSG.PZ_REQ_POSCONTROLMODE, - MGMSG.PZ_GET_POSCONTROLMODE, 1) + [MGMSG.PZ_GET_POSCONTROLMODE], 1) return get_msg.param2 def set_output_volts(self, voltage): @@ -372,8 +372,6 @@ class Tpz(Tcube): between the three values 75 V, 100 V and 150 V. """ - voltage = strip_unit(voltage, "V") - if voltage < 0 or voltage > self.voltage_limit: raise ValueError("Voltage must be in range [0;{}]" .format(self.voltage_limit)) @@ -389,8 +387,8 @@ class Tpz(Tcube): """ get_msg = self.send_request(MGMSG.PZ_REQ_OUTPUTVOLTS, - MGMSG.PZ_GET_OUTPUTVOLTS, 1) - return st.unpack("` method. """ - output = strip_unit(output, "V") volt = round(output*32767/self.voltage_limit) payload = st.pack("` for the meaning of those parameters. - :rtype: a 2 elements tuple (Quantity, int) + :rtype: a 2 elements tuple (int, int) """ get_msg = self.send_request(MGMSG.PZ_REQ_TPZ_IOSETTINGS, - MGMSG.PZ_GET_TPZ_IOSETTINGS, 1) + [MGMSG.PZ_GET_TPZ_IOSETTINGS], 1) voltage_limit, hub_analog_input = st.unpack("= self.depth: + del self.data[0] + self.data.append((rid, message)) + log.worker_pass_rid = True + + def main(): args = get_argparser().parse_args() - init_logger(args) - ddb = FlatFileDB("ddb.pyon") - pdb = FlatFileDB("pdb.pyon") - simplephist = SimpleHistory(30) - pdb.hooks.append(simplephist) - rtr = RTResults() - repository = Repository() - if os.name == "nt": loop = asyncio.ProactorEventLoop() asyncio.set_event_loop(loop) @@ -48,23 +52,31 @@ def main(): loop = asyncio.get_event_loop() atexit.register(lambda: loop.close()) + ddb = FlatFileDB("ddb.pyon") + pdb = FlatFileDB("pdb.pyon") + rtr = Notifier(dict()) + log = Log(1000) + worker_handlers = { - "req_device": ddb.request, - "req_parameter": pdb.request, + "get_device": ddb.get, + "get_parameter": pdb.get, "set_parameter": pdb.set, - "init_rt_results": rtr.init, - "update_rt_results": rtr.update, + "update_rt_results": lambda mod: process_mod(rtr, mod), + "log": log.log } scheduler = Scheduler(get_last_rid() + 1, worker_handlers) worker_handlers["scheduler_submit"] = scheduler.submit scheduler.start() atexit.register(lambda: loop.run_until_complete(scheduler.stop())) + repository = Repository(log.log) + repository.scan_async() + server_control = Server({ "master_ddb": ddb, "master_pdb": pdb, "master_schedule": scheduler, - "master_repository": repository, + "master_repository": repository }) loop.run_until_complete(server_control.start( args.bind, args.port_control)) @@ -74,9 +86,9 @@ def main(): "schedule": scheduler.notifier, "devices": ddb.data, "parameters": pdb.data, - "parameters_simplehist": simplephist.history, - "rt_results": rtr.groups, - "explist": repository.explist + "rt_results": rtr, + "explist": repository.explist, + "log": log.data }) loop.run_until_complete(server_notify.start( args.bind, args.port_notify)) diff --git a/artiq/frontend/artiq_mkfs.py b/artiq/frontend/artiq_mkfs.py index ac00f632d..22c21f1d0 100755 --- a/artiq/frontend/artiq_mkfs.py +++ b/artiq/frontend/artiq_mkfs.py @@ -20,16 +20,13 @@ def get_argparser(): def write_record(f, key, value): + key_size = len(key) + 1 + value_size = len(value) + record_size = key_size + value_size + 4 + f.write(struct.pack(">l", record_size)) f.write(key.encode()) f.write(b"\x00") - key_size = len(key) + 1 - if key_size % 4: - f.write(bytes(4 - (key_size % 4))) - f.write(struct.pack(">l", len(value))) f.write(value) - value_size = len(value) - if value_size % 4: - f.write(bytes(4 - (value_size % 4))) def write_end_marker(f): diff --git a/artiq/frontend/artiq_rpctool.py b/artiq/frontend/artiq_rpctool.py index bdecadb89..a26d70cf7 100755 --- a/artiq/frontend/artiq_rpctool.py +++ b/artiq/frontend/artiq_rpctool.py @@ -36,8 +36,11 @@ def list_targets(target_names, id_parameters): def list_methods(remote): - methods = remote.get_rpc_method_list() - for name, (argspec, docstring) in sorted(methods.items()): + doc = remote.get_rpc_method_list() + if doc["docstring"] is not None: + print(doc["docstring"]) + print() + for name, (argspec, docstring) in sorted(doc["methods"].items()): args = "" for arg in argspec["args"]: args += arg diff --git a/artiq/frontend/artiq_run.py b/artiq/frontend/artiq_run.py index 9b499fb96..baace1d0e 100755 --- a/artiq/frontend/artiq_run.py +++ b/artiq/frontend/artiq_run.py @@ -9,20 +9,19 @@ import logging import h5py -from artiq.language.db import * -from artiq.language.experiment import Experiment +from artiq.language.environment import EnvExperiment from artiq.protocols.file_db import FlatFileDB -from artiq.master.worker_db import DBHub, ResultDB +from artiq.master.worker_db import DeviceManager, ResultDB from artiq.tools import * logger = logging.getLogger(__name__) -class ELFRunner(Experiment, AutoDB): - class DBKeys: - core = Device() - file = Argument() +class ELFRunner(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("file") def run(self): with open(self.file, "rb") as f: @@ -36,42 +35,21 @@ class SimpleParamLogger: logger.info("Parameter change: {} = {}".format(name, value)) -class DummyWatchdog: - def __init__(self, t): - pass - - def __enter__(self): - pass - - def __exit__(self, type, value, traceback): - pass - - class DummyScheduler: def __init__(self): self.next_rid = 0 - self.next_trid = 0 + self.pipeline_name = "main" + self.priority = 0 + self.expid = None - def run_queued(self, run_params): + def submit(self, pipeline_name, expid, priority, due_date, flush): rid = self.next_rid self.next_rid += 1 - logger.info("Queuing: %s, RID=%s", run_params, rid) + logger.info("Submitting: %s, RID=%s", expid, rid) return rid - def cancel_queued(self, rid): - logger.info("Cancelling RID %s", rid) - - def run_timed(self, run_params, next_run): - trid = self.next_trid - self.next_trid += 1 - next_run_s = time.strftime("%m/%d %H:%M:%S", time.localtime(next_run)) - logger.info("Timing: %s at %s, TRID=%s", run_params, next_run_s, trid) - return trid - - def cancel_timed(self, trid): - logger.info("Cancelling TRID %s", trid) - - watchdog = DummyWatchdog + def delete(self, rid): + logger.info("Deleting RID %s", rid) def get_argparser(with_file=True): @@ -98,7 +76,7 @@ def get_argparser(with_file=True): return parser -def _build_experiment(dbh, args): +def _build_experiment(dmgr, pdb, rdb, args): if hasattr(args, "file"): if args.file.endswith(".elf"): if args.arguments: @@ -106,7 +84,7 @@ def _build_experiment(dbh, args): if args.experiment: raise ValueError("experiment-by-name not supported " "for ELF kernels") - return ELFRunner(dbh, file=args.file) + return ELFRunner(dmgr, pdb, rdb, file=args.file) else: module = file_import(args.file) file = args.file @@ -115,37 +93,38 @@ def _build_experiment(dbh, args): file = getattr(module, "__file__") exp = get_experiment(module, args.experiment) arguments = parse_arguments(args.arguments) - return exp(dbh, - scheduler=DummyScheduler(), - run_params=dict(file=file, - experiment=args.experiment, - arguments=arguments), - **arguments) + expid = { + "file": file, + "experiment": args.experiment, + "arguments": arguments + } + dmgr.virtual_devices["scheduler"].expid = expid + return exp(dmgr, pdb, rdb, **arguments) def run(with_file=False): args = get_argparser(with_file).parse_args() init_logger(args) - ddb = FlatFileDB(args.ddb) + dmgr = DeviceManager(FlatFileDB(args.ddb), + virtual_devices={"scheduler": DummyScheduler()}) pdb = FlatFileDB(args.pdb) pdb.hooks.append(SimpleParamLogger()) - rdb = ResultDB(lambda description: None, lambda mod: None) - dbh = DBHub(ddb, pdb, rdb) + rdb = ResultDB() try: - exp_inst = _build_experiment(dbh, args) - rdb.build() + exp_inst = _build_experiment(dmgr, pdb, rdb, args) + exp_inst.prepare() exp_inst.run() exp_inst.analyze() finally: - dbh.close_devices() + dmgr.close_devices() if args.hdf5 is not None: with h5py.File(args.hdf5, "w") as f: rdb.write_hdf5(f) - elif rdb.data.read or rdb.realtime_data.read: - r = chain(rdb.realtime_data.read.items(), rdb.data.read.items()) + elif rdb.rt.read or rdb.nrt: + r = chain(rdb.rt.read.items(), rdb.nrt.items()) for k, v in sorted(r, key=itemgetter(0)): print("{}: {}".format(k, v)) diff --git a/artiq/frontend/lda_controller.py b/artiq/frontend/lda_controller.py index 8efc3cbb4..8286bc143 100755 --- a/artiq/frontend/lda_controller.py +++ b/artiq/frontend/lda_controller.py @@ -15,8 +15,14 @@ def get_argparser(): choices=["LDA-102", "LDA-602"]) simple_network_args(parser, 3253) parser.add_argument("-d", "--device", default=None, - help="USB serial number of the device." - " Omit for simulation mode.") + help="USB serial number of the device. " + "The serial number is written on a sticker under " + "the device, you should write for example " + "-d \"SN:03461\". You must prepend enough 0s for " + "it to be 5 digits. If omitted, the first " + "available device will be used.") + parser.add_argument("--simulation", action="store_true", + help="Put the driver in simulation mode.") verbosity_args(parser) return parser @@ -24,7 +30,7 @@ def get_argparser(): def main(): args = get_argparser().parse_args() init_logger(args) - if args.device is None: + if args.simulation: lda = Ldasim() else: lda = Lda(args.device, args.product) diff --git a/artiq/frontend/novatech409b_controller.py b/artiq/frontend/novatech409b_controller.py index 30dcd75f7..57846ae95 100755 --- a/artiq/frontend/novatech409b_controller.py +++ b/artiq/frontend/novatech409b_controller.py @@ -4,6 +4,7 @@ import argparse import logging +import sys from artiq.devices.novatech409b.driver import Novatech409B from artiq.protocols.pc_rpc import simple_server_loop @@ -19,7 +20,10 @@ def get_argparser(): simple_network_args(parser, 3254) parser.add_argument( "-d", "--device", default=None, - help="serial port. Omit for simulation mode.") + help="serial port.") + parser.add_argument( + "--simulation", action="store_true", + help="Put the driver in simulation mode, even if --device is used.") verbosity_args(parser) return parser @@ -28,7 +32,12 @@ def main(): args = get_argparser().parse_args() init_logger(args) - dev = Novatech409B(args.device) + if not args.simulation and args.device is None: + print("You need to specify either --simulation or -d/--device " + "argument. Use --help for more information.") + sys.exit(1) + + dev = Novatech409B(args.device if not args.simulation else None) try: simple_server_loop( {"novatech409b": dev}, args.bind, args.port) diff --git a/artiq/frontend/pdq2_controller.py b/artiq/frontend/pdq2_controller.py index 6404d7d7f..577cdce2b 100755 --- a/artiq/frontend/pdq2_controller.py +++ b/artiq/frontend/pdq2_controller.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import sys from artiq.devices.pdq2.driver import Pdq2 from artiq.protocols.pc_rpc import simple_server_loop @@ -12,7 +13,10 @@ def get_argparser(): simple_network_args(parser, 3252) parser.add_argument( "-d", "--device", default=None, - help="serial port. Omit for simulation mode.") + help="serial port.") + parser.add_argument( + "--simulation", action="store_true", + help="Put the driver in simulation mode, even if --device is used.") parser.add_argument( "--dump", default="pdq2_dump.bin", help="file to dump pdq2 data into, for later simulation") @@ -24,7 +28,13 @@ def main(): args = get_argparser().parse_args() init_logger(args) port = None - if args.device is None: + + if not args.simulation and args.device is None: + print("You need to specify either --simulation or -d/--device " + "argument. Use --help for more information.") + sys.exit(1) + + if args.simulation: port = open(args.dump, "wb") dev = Pdq2(url=args.device, dev=port) try: diff --git a/artiq/frontend/pxi6733_controller.py b/artiq/frontend/pxi6733_controller.py index 755dfa2bd..9658dc6bb 100755 --- a/artiq/frontend/pxi6733_controller.py +++ b/artiq/frontend/pxi6733_controller.py @@ -2,6 +2,7 @@ # Yann Sionneau , 2015 import argparse +import sys from artiq.protocols.pc_rpc import simple_server_loop from artiq.devices.pxi6733.driver import DAQmx, DAQmxSim @@ -11,13 +12,13 @@ from artiq.tools import verbosity_args, init_logger, simple_network_args def get_argparser(): parser = argparse.ArgumentParser(description="NI PXI 6733 controller") simple_network_args(parser, 3256) - parser.add_argument("-d", "--device", default=None, - help="Device name (e.g. Dev1)." - " Omit for simulation mode.") + parser.add_argument("-C", "--channels", default=None, + help="List of channels (e.g. Dev1/ao0, Dev1/ao1:3).") parser.add_argument("-c", "--clock", default="PFI5", help="Input clock pin name (default: PFI5)") - parser.add_argument("-a", "--analog-output", default="ao0", - help="Analog output pin name (default: ao0)") + parser.add_argument("--simulation", action='store_true', + help="Put the driver in simulation mode, even if " + "--channels is used.") verbosity_args(parser) return parser @@ -26,12 +27,16 @@ def main(): args = get_argparser().parse_args() init_logger(args) - if args.device is None: + if not args.simulation and args.channels is None: + print("You need to specify either --simulation or -C/--channels " + "argument. Use --help for more information.") + sys.exit(1) + + if args.simulation: daq = DAQmxSim() else: - daq = DAQmx(bytes(args.device, "ascii"), - bytes(args.analog_output, "ascii"), - bytes(args.clock, "ascii")) + daq = DAQmx(args.channels, + args.clock) try: simple_server_loop({"pxi6733": daq}, diff --git a/artiq/frontend/thorlabs_tcube_controller.py b/artiq/frontend/thorlabs_tcube_controller.py index d4dc18388..85ccc6ebc 100755 --- a/artiq/frontend/thorlabs_tcube_controller.py +++ b/artiq/frontend/thorlabs_tcube_controller.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import argparse +import sys from artiq.devices.thorlabs_tcube.driver import Tdc, Tpz, TdcSim, TpzSim from artiq.protocols.pc_rpc import simple_server_loop @@ -13,7 +14,11 @@ def get_argparser(): help="type of the Thorlabs T-Cube device to control", choices=["TDC001", "TPZ001"]) parser.add_argument("-d", "--device", default=None, - help="serial port. Omit for simulation mode.") + help="serial device. See documentation for how to " + "specify a USB Serial Number.") + parser.add_argument("--simulation", action="store_true", + help="Put the driver in simulation mode, even if " + "--device is used.") simple_network_args(parser, 3255) verbosity_args(parser) return parser @@ -23,7 +28,12 @@ def main(): args = get_argparser().parse_args() init_logger(args) - if args.device is None: + if not args.simulation and args.device is None: + print("You need to specify either --simulation or -d/--device " + "argument. Use --help for more information.") + sys.exit(1) + + if args.simulation: if args.product == "TDC001": dev = TdcSim() elif args.product == "TPZ001": diff --git a/artiq/gateware/ad9858.py b/artiq/gateware/ad9xxx.py similarity index 81% rename from artiq/gateware/ad9858.py rename to artiq/gateware/ad9xxx.py index d620cac43..aa087053f 100644 --- a/artiq/gateware/ad9858.py +++ b/artiq/gateware/ad9xxx.py @@ -6,15 +6,14 @@ from migen.bus.transactions import * from migen.sim.generic import run_simulation -class AD9858(Module): - """Wishbone interface to the AD9858 DDS chip. +class AD9xxx(Module): + """Wishbone interface to the AD9858 and AD9914 DDS chips. - Addresses 0-63 map the AD9858 registers. - Data is zero-padded. + Addresses 0-2**flen(pads.a)-1 map the AD9xxx registers. - Write to address 64 to pulse the FUD signal. - Address 65 is a GPIO register that controls the sel, p and reset signals. - sel is mapped to the lower bits, followed by p and reset. + Write to address 2**flen(pads.a) to pulse the FUD signal. + Address 2**flen(pads.a)+1 is a GPIO register that controls the + sel and reset signals. rst is mapped to bit 0, followed by sel. Write timing: Address is set one cycle before assertion of we_n. @@ -28,6 +27,7 @@ class AD9858(Module): Design: All IO pads are registered. + With QC1 adapter: LVDS driver/receiver propagation delays are 3.6+4.5 ns max LVDS state transition delays are 20, 15 ns max Schmitt trigger delays are 6.4ns max @@ -38,15 +38,15 @@ class AD9858(Module): read_wait_cycles=10, hiz_wait_cycles=3, bus=None): if bus is None: - bus = wishbone.Interface() + bus = wishbone.Interface(data_width=flen(pads.d)) self.bus = bus # # # - dts = TSTriple(8) + dts = TSTriple(flen(pads.d)) self.specials += dts.get_tristate(pads.d) hold_address = Signal() - dr = Signal(8) + dr = Signal(flen(pads.d)) rx = Signal() self.sync += [ If(~hold_address, pads.a.eq(bus.adr)), @@ -55,13 +55,14 @@ class AD9858(Module): dts.oe.eq(~rx) ] - gpio = Signal(flen(pads.sel) + flen(pads.p) + 1) + gpio = Signal(flen(pads.sel) + 1) gpio_load = Signal() self.sync += If(gpio_load, gpio.eq(bus.dat_w)) - self.comb += [ - Cat(pads.sel, pads.p).eq(gpio), - pads.rst_n.eq(~gpio[-1]), - ] + if hasattr(pads, "rst"): + self.comb += pads.rst.eq(gpio[0]) + else: + self.comb += pads.rst_n.eq(~gpio[0]) + self.comb += pads.sel.eq(gpio[1:]) bus_r_gpio = Signal() self.comb += If(bus_r_gpio, @@ -71,7 +72,10 @@ class AD9858(Module): ) fud = Signal() - self.sync += pads.fud_n.eq(~fud) + if hasattr(pads, "fud"): + self.sync += pads.fud.eq(fud) + else: + self.sync += pads.fud_n.eq(~fud) pads.wr_n.reset = 1 pads.rd_n.reset = 1 @@ -87,7 +91,7 @@ class AD9858(Module): fsm.act("IDLE", If(bus.cyc & bus.stb, - If(bus.adr[6], + If(bus.adr[flen(pads.a)], If(bus.adr[0], NextState("GPIO") ).Else( @@ -168,7 +172,6 @@ class _TestPads: self.a = Signal(6) self.d = Signal(8) self.sel = Signal(5) - self.p = Signal(2) self.fud_n = Signal() self.wr_n = Signal() self.rd_n = Signal() @@ -178,11 +181,11 @@ class _TestPads: class _TB(Module): def __init__(self): pads = _TestPads() - self.submodules.dut = AD9858(pads, drive_fud=True) + self.submodules.dut = AD9xxx(pads, drive_fud=True) self.submodules.initiator = wishbone.Initiator(_test_gen()) self.submodules.interconnect = wishbone.InterconnectPointToPoint( self.initiator.bus, self.dut.bus) if __name__ == "__main__": - run_simulation(_TB(), vcd_name="ad9858.vcd") + run_simulation(_TB(), vcd_name="ad9xxx.vcd") diff --git a/artiq/gateware/amp/kernel_cpu.py b/artiq/gateware/amp/kernel_cpu.py index b66641997..7afd5730d 100644 --- a/artiq/gateware/amp/kernel_cpu.py +++ b/artiq/gateware/amp/kernel_cpu.py @@ -3,12 +3,11 @@ from migen.bank.description import * from migen.bus import wishbone from misoclib.cpu import mor1kx -from misoclib.mem.sdram.frontend.wishbone2lasmi import WB2LASMI from misoclib.soc import mem_decoder class KernelCPU(Module): - def __init__(self, platform, lasmim, + def __init__(self, platform, exec_address=0x40400000, main_mem_origin=0x40000000, l2_size=8192): @@ -29,16 +28,8 @@ class KernelCPU(Module): "sys_kernel") # DRAM access - # XXX Vivado 2014.X workaround - from mibuild.xilinx.vivado import XilinxVivadoToolchain - if isinstance(platform.toolchain, XilinxVivadoToolchain): - from migen.fhdl.simplify import FullMemoryWE - self.submodules.wishbone2lasmi = FullMemoryWE()( - WB2LASMI(l2_size//4, lasmim)) - else: - self.submodules.wishbone2lasmi = WB2LASMI(l2_size//4, lasmim) - self.add_wb_slave(mem_decoder(main_mem_origin), - self.wishbone2lasmi.wishbone) + self.wb_sdram = wishbone.Interface() + self.add_wb_slave(mem_decoder(main_mem_origin), self.wb_sdram) def get_csrs(self): return [self._reset] diff --git a/artiq/gateware/nist_qc1.py b/artiq/gateware/nist_qc1.py index a2d68e44f..f75f52371 100644 --- a/artiq/gateware/nist_qc1.py +++ b/artiq/gateware/nist_qc1.py @@ -4,10 +4,22 @@ from mibuild.generic_platform import * papilio_adapter_io = [ ("ext_led", 0, Pins("B:7"), IOStandard("LVTTL")), + # to feed the 125 MHz clock (preferrably from DDS SYNC_CLK) + # to the FPGA, use the xtrig pair. + # + # on papiliopro-adapter, xtrig (C:12) is connected to a GCLK + # + # on pipistrello, C:15 is the only GCLK in proximity, used as a button + # input, BTN2/PMT2 in papiliopro-adapter + # either improve the DDS box to feed 125MHz into the PMT2 pair, or: + # + # * disconnect C:15 from its periphery on the adapter board + # * bridge C:15 to the xtrig output of the transciever + # * optionally, disconnect C:12 from its periphery + ("xtrig", 0, Pins("C:12"), IOStandard("LVTTL")), ("pmt", 0, Pins("C:13"), IOStandard("LVTTL")), ("pmt", 1, Pins("C:14"), IOStandard("LVTTL")), - ("xtrig", 0, Pins("C:12"), IOStandard("LVTTL")), - ("dds_clock", 0, Pins("C:15"), IOStandard("LVTTL")), # PMT2 + ("pmt", 2, Pins("C:15"), IOStandard("LVTTL")), # rarely equipped ("ttl", 0, Pins("C:11"), IOStandard("LVTTL")), ("ttl", 1, Pins("C:10"), IOStandard("LVTTL")), diff --git a/artiq/gateware/nist_qc2.py b/artiq/gateware/nist_qc2.py new file mode 100644 index 000000000..9d6896781 --- /dev/null +++ b/artiq/gateware/nist_qc2.py @@ -0,0 +1,78 @@ +from mibuild.generic_platform import * + + +fmc_adapter_io = [ + ("ttl", 0, Pins("LPC:LA00_CC_P"), IOStandard("LVTTL")), + ("ttl", 1, Pins("LPC:LA02_P"), IOStandard("LVTTL")), + ("ttl", 2, Pins("LPC:LA00_CC_N"), IOStandard("LVTTL")), + ("ttl", 3, Pins("LPC:LA02_N"), IOStandard("LVTTL")), + ("ttl", 4, Pins("LPC:LA01_CC_P"), IOStandard("LVTTL")), + ("ttl", 5, Pins("LPC:LA01_CC_N"), IOStandard("LVTTL")), + ("ttl", 6, Pins("LPC:LA06_P"), IOStandard("LVTTL")), + ("ttl", 7, Pins("LPC:LA06_N"), IOStandard("LVTTL")), + ("ttl", 8, Pins("LPC:LA05_P"), IOStandard("LVTTL")), + ("ttl", 9, Pins("LPC:LA05_N"), IOStandard("LVTTL")), + ("ttl", 10, Pins("LPC:LA10_P"), IOStandard("LVTTL")), + ("ttl", 11, Pins("LPC:LA09_P"), IOStandard("LVTTL")), + ("ttl", 12, Pins("LPC:LA10_N"), IOStandard("LVTTL")), + ("ttl", 13, Pins("LPC:LA09_N"), IOStandard("LVTTL")), + ("ttl", 14, Pins("LPC:LA13_P"), IOStandard("LVTTL")), + ("ttl", 15, Pins("LPC:LA14_P"), IOStandard("LVTTL")), + + ("dds", 0, + Subsignal("a", Pins("LPC:LA22_N LPC:LA21_P LPC:LA22_P LPC:LA19_N " + "LPC:LA20_N LPC:LA19_P LPC:LA20_P")), + Subsignal("d", Pins("LPC:LA15_N LPC:LA16_N LPC:LA15_P LPC:LA16_P " + "LPC:LA11_N LPC:LA12_N LPC:LA11_P LPC:LA12_P " + "LPC:LA07_N LPC:LA08_N LPC:LA07_P LPC:LA08_P " + "LPC:LA04_N LPC:LA03_N LPC:LA04_P LPC:LA03_P")), + Subsignal("sel", Pins("LPC:LA24_N LPC:LA29_P LPC:LA28_P LPC:LA29_N " + "LPC:LA28_N LPC:LA31_P LPC:LA30_P LPC:LA31_N " + "LPC:LA30_N LPC:LA33_P LPC:LA33_N")), + Subsignal("fud", Pins("LPC:LA21_N")), + Subsignal("wr_n", Pins("LPC:LA24_P")), + Subsignal("rd_n", Pins("LPC:LA25_N")), + Subsignal("rst", Pins("LPC:LA25_P")), + IOStandard("LVTTL")), + + ("i2c", 0, + Subsignal("scl", Pins("LPC:IIC_SLC")), + Subsignal("sda", Pins("LPC:IIC_SDA")), + IOStandard("LVCMOS25")), + + ("clk_m2c", 0, + Subsignal("p", Pins("LPC:CLK0_M2C_P")), + Subsignal("n", Pins("LPC:CLK0_M2C_N")), + IOStandard("LVDS")), + + ("clk_m2c", 1, + Subsignal("p", Pins("LPC:CLK1_M2C_P")), + Subsignal("n", Pins("LPC:CLK1_M2C_N")), + IOStandard("LVDS")), + + ("la32", 0, + Subsignal("p", Pins("LPC:LA32_P")), + Subsignal("n", Pins("LPC:LA32_N")), + IOStandard("LVDS")), + + ("spi", 0, + Subsignal("clk", Pins("LPC:LA13_N")), + Subsignal("ce", Pins("LPC:LA14_N")), + Subsignal("mosi", Pins("LPC:LA17_CC_P")), + Subsignal("miso", Pins("LPC:LA17_CC_N")), + IOStandard("LVTTL")), + + ("spi", 1, + Subsignal("clk", Pins("LPC:LA18_CC_P")), + Subsignal("ce", Pins("LPC:LA18_CC_N")), + Subsignal("mosi", Pins("LPC:LA23_P")), + Subsignal("miso", Pins("LPC:LA23_N")), + IOStandard("LVTTL")), + + ("spi", 2, + Subsignal("clk", Pins("LPC:LA27_P")), + Subsignal("ce", Pins("LPC:LA26_P")), + Subsignal("mosi", Pins("LPC:LA27_N")), + Subsignal("miso", Pins("LPC:LA26_N")), + IOStandard("LVTTL")), +] diff --git a/artiq/gateware/rtio/__init__.py b/artiq/gateware/rtio/__init__.py index 9b34976c0..b02ac0d3a 100644 --- a/artiq/gateware/rtio/__init__.py +++ b/artiq/gateware/rtio/__init__.py @@ -1 +1,2 @@ from artiq.gateware.rtio.core import Channel, RTIO +from artiq.gateware.rtio.moninj import MonInj diff --git a/artiq/gateware/rtio/core.py b/artiq/gateware/rtio/core.py index cfa7ea01e..b4fedcf88 100644 --- a/artiq/gateware/rtio/core.py +++ b/artiq/gateware/rtio/core.py @@ -44,7 +44,8 @@ class _RTIOCounter(Module): # # # - self.sync.rio += self.value_rio.eq(self.value_rio + 1), + # note: counter is in rtio domain and never affected by the reset CSRs + self.sync.rtio += self.value_rio.eq(self.value_rio + 1) gt = _GrayCodeTransfer(width) self.submodules += gt self.comb += gt.i.eq(self.value_rio), self.value_sys.eq(gt.o) @@ -121,12 +122,20 @@ class _OutputManager(Module): sequence_error.eq(self.ev.timestamp < buf.timestamp[fine_ts_width:]) ] if interface.suppress_nop: - self.sync.rsys += nop.eq( - optree("&", - [getattr(self.ev, a) == getattr(buf, a) - for a in ("data", "address") - if hasattr(self.ev, a)], - default=0)) + # disable NOP at reset: do not suppress a first write with all 0s + nop_en = Signal(reset=0) + self.sync.rsys += [ + nop.eq(nop_en & + optree("&", + [getattr(self.ev, a) == getattr(buf, a) + for a in ("data", "address") + if hasattr(self.ev, a)], + default=0)), + # buf now contains valid data. enable NOP. + If(self.we & ~sequence_error, nop_en.eq(1)), + # underflows cancel the write. allow it to be retried. + If(self.underflow, nop_en.eq(0)) + ] self.comb += self.sequence_error.eq(self.we & sequence_error) # Buffer read and FIFO write @@ -247,11 +256,20 @@ class _InputManager(Module): class Channel: - def __init__(self, interface, ofifo_depth=64, ififo_depth=64): + def __init__(self, interface, probes=[], overrides=[], + ofifo_depth=64, ififo_depth=64): self.interface = interface + self.probes = probes + self.overrides = overrides self.ofifo_depth = ofifo_depth self.ififo_depth = ififo_depth + @classmethod + def from_phy(cls, phy, **kwargs): + probes = getattr(phy, "probes", []) + overrides = getattr(phy, "overrides", []) + return cls(phy.rtlink, probes, overrides, **kwargs) + class _KernelCSRs(AutoCSR): def __init__(self, chan_sel_width, diff --git a/artiq/gateware/rtio/moninj.py b/artiq/gateware/rtio/moninj.py new file mode 100644 index 000000000..9bb0d0283 --- /dev/null +++ b/artiq/gateware/rtio/moninj.py @@ -0,0 +1,68 @@ +from migen.fhdl.std import * +from migen.bank.description import * +from migen.genlib.cdc import BusSynchronizer, MultiReg + + +class Monitor(Module, AutoCSR): + def __init__(self, channels): + chan_probes = [c.probes for c in channels] + + max_chan_probes = max(len(cp) for cp in chan_probes) + max_probe_len = max(flen(p) for cp in chan_probes for p in cp) + self.chan_sel = CSRStorage(bits_for(len(chan_probes)-1)) + self.probe_sel = CSRStorage(bits_for(max_chan_probes-1)) + self.value_update = CSR() + self.value = CSRStatus(max_probe_len) + + # # # + + chan_probes_sys = [] + for cp in chan_probes: + cp_sys = [] + for p in cp: + vs = BusSynchronizer(flen(p), "rio", "rsys") + self.submodules += vs + self.comb += vs.i.eq(p) + cp_sys.append(vs.o) + cp_sys += [0]*(max_chan_probes-len(cp)) + chan_probes_sys.append(Array(cp_sys)[self.probe_sel.storage]) + self.sync += If(self.value_update.re, + self.value.status.eq( + Array(chan_probes_sys)[self.chan_sel.storage])) + + +class Injector(Module, AutoCSR): + def __init__(self, channels): + chan_overrides = [c.overrides for c in channels] + + max_chan_overrides = max(len(co) for co in chan_overrides) + max_override_len = max(flen(o) for co in chan_overrides for o in co) + self.chan_sel = CSRStorage(bits_for(len(chan_overrides)-1)) + self.override_sel = CSRStorage(bits_for(max_chan_overrides-1)) + self.value = CSR(max_override_len) + + # # # + + chan_overrides_sys = [] + for n_channel, co in enumerate(chan_overrides): + co_sys = [] + for n_override, o in enumerate(co): + # We do the clock domain transfer with a simple double-latch. + # Software has to ensure proper timing of any strobe signal etc. + # to avoid problematic glitches. + o_sys = Signal.like(o) + self.specials += MultiReg(o_sys, o, "rio") + self.sync += If(self.value.re & (self.chan_sel.storage == n_channel) + & (self.override_sel.storage == n_override), + o_sys.eq(self.value.r)) + co_sys.append(o_sys) + co_sys += [0]*(max_chan_overrides-len(co)) + chan_overrides_sys.append(Array(co_sys)[self.override_sel.storage]) + self.comb += self.value.w.eq( + Array(chan_overrides_sys)[self.chan_sel.storage]) + + +class MonInj(Module, AutoCSR): + def __init__(self, channels): + self.submodules.mon = Monitor(channels) + self.submodules.inj = Injector(channels) diff --git a/artiq/gateware/rtio/phy/dds.py b/artiq/gateware/rtio/phy/dds.py new file mode 100644 index 000000000..8568c57e1 --- /dev/null +++ b/artiq/gateware/rtio/phy/dds.py @@ -0,0 +1,60 @@ +from migen.fhdl.std import * + +from artiq.gateware import ad9xxx +from artiq.gateware.rtio.phy.wishbone import RT2WB + + +class _AD9xxx(Module): + def __init__(self, ftw_base, pads, nchannels, **kwargs): + self.submodules._ll = RenameClockDomains( + ad9xxx.AD9xxx(pads, **kwargs), "rio") + self.submodules._rt2wb = RT2WB(flen(pads.a)+1, self._ll.bus) + self.rtlink = self._rt2wb.rtlink + self.probes = [Signal(32) for i in range(nchannels)] + + # # # + + # buffer the current address/data on the rtlink output + current_address = Signal.like(self.rtlink.o.address) + current_data = Signal.like(self.rtlink.o.data) + self.sync.rio += If(self.rtlink.o.stb, + current_address.eq(self.rtlink.o.address), + current_data.eq(self.rtlink.o.data)) + + # keep track of the currently selected channel + current_channel = Signal(max=nchannels) + self.sync.rio += If(current_address == 2**flen(pads.a) + 1, + current_channel.eq(current_data)) + + # keep track of frequency tuning words, before they are FUDed + ftws = [Signal(32) for i in range(nchannels)] + for c, ftw in enumerate(ftws): + if flen(pads.d) == 8: + self.sync.rio += \ + If(current_channel == c, [ + If(current_address == ftw_base+i, + ftw[i*8:(i+1)*8].eq(current_data)) + for i in range(4)]) + elif flen(pads.d) == 16: + self.sync.rio += \ + If(current_channel == c, [ + If(current_address == ftw_base+2*i, + ftw[i*16:(i+1)*16].eq(current_data)) + for i in range(2)]) + else: + raise NotImplementedError + + # FTW to probe on FUD + self.sync.rio += If(current_address == 2**flen(pads.a), [ + If(current_channel == c, probe.eq(ftw)) + for c, (probe, ftw) in enumerate(zip(self.probes, ftws))]) + + +class AD9858(_AD9xxx): + def __init__(self, pads, nchannels, **kwargs): + _AD9xxx.__init__(self, 0x0a, pads, nchannels, **kwargs) + + +class AD9914(_AD9xxx): + def __init__(self, pads, nchannels, **kwargs): + _AD9xxx.__init__(self, 0x2d, pads, nchannels, **kwargs) diff --git a/artiq/gateware/rtio/phy/ttl_simple.py b/artiq/gateware/rtio/phy/ttl_simple.py index ce6f953cb..d424babe7 100644 --- a/artiq/gateware/rtio/phy/ttl_simple.py +++ b/artiq/gateware/rtio/phy/ttl_simple.py @@ -7,10 +7,24 @@ from artiq.gateware.rtio import rtlink class Output(Module): def __init__(self, pad): self.rtlink = rtlink.Interface(rtlink.OInterface(1)) + self.probes = [pad] + override_en = Signal() + override_o = Signal() + self.overrides = [override_en, override_o] # # # - self.sync.rio_phy += If(self.rtlink.o.stb, pad.eq(self.rtlink.o.data)) + pad_k = Signal() + self.sync.rio_phy += [ + If(self.rtlink.o.stb, + pad_k.eq(self.rtlink.o.data) + ), + If(override_en, + pad.eq(override_o) + ).Else( + pad.eq(pad_k) + ) + ] class Inout(Module): @@ -18,6 +32,11 @@ class Inout(Module): self.rtlink = rtlink.Interface( rtlink.OInterface(2, 2), rtlink.IInterface(1)) + override_en = Signal() + override_o = Signal() + override_oe = Signal() + self.overrides = [override_en, override_o, override_oe] + self.probes = [] # # # @@ -25,10 +44,21 @@ class Inout(Module): self.specials += ts.get_tristate(pad) sensitivity = Signal(2) - self.sync.rio_phy += If(self.rtlink.o.stb, - If(self.rtlink.o.address == 0, ts.o.eq(self.rtlink.o.data[0])), - If(self.rtlink.o.address == 1, ts.oe.eq(self.rtlink.o.data[0])), + o_k = Signal() + oe_k = Signal() + self.sync.rio_phy += [ + If(self.rtlink.o.stb, + If(self.rtlink.o.address == 0, o_k.eq(self.rtlink.o.data[0])), + If(self.rtlink.o.address == 1, oe_k.eq(self.rtlink.o.data[0])), + ), + If(override_en, + ts.o.eq(override_o), + ts.oe.eq(override_oe) + ).Else( + ts.o.eq(o_k), + ts.oe.eq(oe_k) ) + ] self.sync.rio += If(self.rtlink.o.stb & (self.rtlink.o.address == 2), sensitivity.eq(self.rtlink.o.data)) @@ -43,3 +73,31 @@ class Inout(Module): ), self.rtlink.i.data.eq(i) ] + + self.probes += [i, ts.oe] + + +class ClockGen(Module): + def __init__(self, pad, ftw_width=24): + self.rtlink = rtlink.Interface( + rtlink.OInterface(ftw_width, suppress_nop=False)) + + # # # + + ftw = Signal(ftw_width) + acc = Signal(ftw_width) + self.sync.rio += If(self.rtlink.o.stb, ftw.eq(self.rtlink.o.data)) + self.sync.rio_phy += [ + acc.eq(acc + ftw), + # rtlink takes precedence over regular acc increments + If(self.rtlink.o.stb, + If(self.rtlink.o.data != 0, + # known phase on frequency write: at rising edge + acc.eq(2**(ftw_width - 1)) + ).Else( + # set output to 0 on stop + acc.eq(0) + ) + ), + pad.eq(acc[-1]) + ] diff --git a/artiq/gateware/soc.py b/artiq/gateware/soc.py index a074da772..15095ea92 100644 --- a/artiq/gateware/soc.py +++ b/artiq/gateware/soc.py @@ -19,8 +19,9 @@ class AMPSoC: self.submodules.timer0 = timer.Timer(width=64) - self.submodules.kernel_cpu = amp.KernelCPU( - self.platform, self.sdram.crossbar.get_master()) + self.submodules.kernel_cpu = amp.KernelCPU(self.platform) + self.add_wb_sdram_if(self.kernel_cpu.wb_sdram) + self.submodules.mailbox = amp.Mailbox() self.add_wb_slave(mem_decoder(self.mem_map["mailbox"]), self.mailbox.i1) diff --git a/artiq/gui/displays.py b/artiq/gui/displays.py new file mode 100644 index 000000000..ca2407871 --- /dev/null +++ b/artiq/gui/displays.py @@ -0,0 +1,130 @@ +from collections import OrderedDict + +from quamash import QtGui +import pyqtgraph as pg +from pyqtgraph import dockarea + + +class _SimpleSettings(QtGui.QDialog): + def __init__(self, parent, prev_name, prev_settings, + result_list, create_cb): + QtGui.QDialog.__init__(self, parent=parent) + self.setWindowTitle(self._window_title) + + grid = QtGui.QGridLayout() + self.setLayout(grid) + + grid.addWidget(QtGui.QLabel("Name:"), 0, 0) + self.name = name = QtGui.QLineEdit() + grid.addWidget(name, 0, 1) + if prev_name is not None: + name.insert(prev_name) + + grid.addWidget(QtGui.QLabel("Result:")) + self.result = result = QtGui.QComboBox() + grid.addWidget(result, 1, 1) + result.addItems(result_list) + result.setEditable(True) + if "result" in prev_settings: + result.setEditText(prev_settings["result"]) + + buttons = QtGui.QDialogButtonBox( + QtGui.QDialogButtonBox.Ok | QtGui.QDialogButtonBox.Cancel) + grid.addWidget(buttons, 2, 0, 1, 2) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + + def on_accept(): + create_cb(name.text(), {"result": result.currentText()}) + self.accepted.connect(on_accept) + + def accept(self): + if self.name.text() and self.result.currentText(): + QtGui.QDialog.accept(self) + + +class NumberDisplaySettings(_SimpleSettings): + _window_title = "Number display" + + +class NumberDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "Display: " + name, size=(250, 250), + closable=True) + self.settings = settings + self.number = QtGui.QLCDNumber() + self.number.setDigitCount(10) + self.addWidget(self.number) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + n = float(data[result]) + except: + n = "---" + self.number.display(n) + + +class XYDisplaySettings(_SimpleSettings): + _window_title = "XY plot" + + +class XYDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "XY: " + name, size=(640, 480), + closable=True) + self.settings = settings + self.plot = pg.PlotWidget() + self.addWidget(self.plot) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + y = data[result] + except KeyError: + return + self.plot.clear() + if not y: + return + self.plot.plot(y) + + +class HistogramDisplaySettings(_SimpleSettings): + _window_title = "Histogram" + + +class HistogramDisplay(dockarea.Dock): + def __init__(self, name, settings): + dockarea.Dock.__init__(self, "Histogram: " + name, size=(640, 480), + closable=True) + self.settings = settings + self.plot = pg.PlotWidget() + self.addWidget(self.plot) + + def data_sources(self): + return {self.settings["result"]} + + def update_data(self, data): + result = self.settings["result"] + try: + y = data[result] + except KeyError: + return + x = list(range(len(y)+1)) + self.plot.clear() + if not y: + return + self.plot.plot(x, y, stepMode=True, fillLevel=0, brush=(0, 0, 255, 150)) + + +display_types = OrderedDict([ + ("Number", (NumberDisplaySettings, NumberDisplay)), + ("XY", (XYDisplaySettings, XYDisplay)), + ("Histogram", (HistogramDisplaySettings, HistogramDisplay)) +]) diff --git a/artiq/gui/explorer.py b/artiq/gui/explorer.py index 9d5603193..1d82b2a99 100644 --- a/artiq/gui/explorer.py +++ b/artiq/gui/explorer.py @@ -1,11 +1,14 @@ import asyncio +import traceback from quamash import QtGui, QtCore from pyqtgraph import dockarea from pyqtgraph import LayoutWidget from artiq.protocols.sync_struct import Subscriber +from artiq.protocols import pyon from artiq.gui.tools import DictSyncModel +from artiq.gui.scan import ScanController class _ExplistModel(DictSyncModel): @@ -21,46 +24,175 @@ class _ExplistModel(DictSyncModel): return k -class ExplorerDock(dockarea.Dock): - def __init__(self, status_bar, schedule_ctl): - dockarea.Dock.__init__(self, "Explorer", size=(1100, 400)) +class _FreeValueEntry(QtGui.QLineEdit): + def __init__(self, procdesc): + QtGui.QLineEdit.__init__(self) + if "default" in procdesc: + self.insert(pyon.encode(procdesc["default"])) + def get_argument_value(self): + return pyon.decode(self.text()) + + +class _BooleanEntry(QtGui.QCheckBox): + def __init__(self, procdesc): + QtGui.QCheckBox.__init__(self) + if "default" in procdesc: + self.setChecked(procdesc["default"]) + + def get_argument_value(self): + return self.isChecked() + + +class _EnumerationEntry(QtGui.QComboBox): + def __init__(self, procdesc): + QtGui.QComboBox.__init__(self) + self.choices = procdesc["choices"] + self.addItems(self.choices) + if "default" in procdesc: + try: + idx = self.choices.index(procdesc["default"]) + except: + pass + else: + self.setCurrentIndex(idx) + + def get_argument_value(self): + return self.choices[self.currentIndex()] + + +class _NumberEntry(QtGui.QDoubleSpinBox): + def __init__(self, procdesc): + QtGui.QDoubleSpinBox.__init__(self) + if procdesc["step"] is not None: + self.setSingleStep(procdesc["step"]) + if procdesc["min"] is not None: + self.setMinimum(procdesc["min"]) + if procdesc["max"] is not None: + self.setMinimum(procdesc["max"]) + if procdesc["unit"]: + self.setSuffix(" " + procdesc["unit"]) + if "default" in procdesc: + self.setValue(procdesc["default"]) + + def get_argument_value(self): + return self.value() + + +class _StringEntry(QtGui.QLineEdit): + def __init__(self, procdesc): + QtGui.QLineEdit.__init__(self) + if "default" in procdesc: + self.insert(procdesc["default"]) + + def get_argument_value(self): + return self.text() + + +_procty_to_entry = { + "FreeValue": _FreeValueEntry, + "BooleanValue": _BooleanEntry, + "EnumerationValue": _EnumerationEntry, + "NumberValue": _NumberEntry, + "StringValue": _StringEntry, + "Scannable": ScanController +} + + +class _ArgumentSetter(LayoutWidget): + def __init__(self, dialog_parent, arguments): + LayoutWidget.__init__(self) + self.dialog_parent = dialog_parent + + if not arguments: + self.addWidget(QtGui.QLabel("No arguments"), 0, 0) + + self._args_to_entries = dict() + for n, (name, procdesc) in enumerate(arguments): + self.addWidget(QtGui.QLabel(name), n, 0) + entry = _procty_to_entry[procdesc["ty"]](procdesc) + self.addWidget(entry, n, 1) + self._args_to_entries[name] = entry + + def get_argument_values(self): + r = dict() + for arg, entry in self._args_to_entries.items(): + try: + r[arg] = entry.get_argument_value() + except: + msgbox = QtGui.QMessageBox(self.dialog_parent) + msgbox.setWindowTitle("Error") + msgbox.setText("Failed to obtain value for argument '{}'.\n{}" + .format(arg, traceback.format_exc())) + msgbox.setStandardButtons(QtGui.QMessageBox.Ok) + msgbox.show() + return None + return r + + +class ExplorerDock(dockarea.Dock): + def __init__(self, dialog_parent, status_bar, schedule_ctl): + dockarea.Dock.__init__(self, "Explorer", size=(1500, 500)) + + self.dialog_parent = dialog_parent self.status_bar = status_bar self.schedule_ctl = schedule_ctl - splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) - self.addWidget(splitter) + self.splitter = QtGui.QSplitter(QtCore.Qt.Horizontal) + self.addWidget(self.splitter) grid = LayoutWidget() - splitter.addWidget(grid) + self.splitter.addWidget(grid) self.el = QtGui.QListView() + self.el.selectionChanged = self.update_argsetter grid.addWidget(self.el, 0, 0, colspan=4) self.datetime = QtGui.QDateTimeEdit() self.datetime.setDisplayFormat("MMM d yyyy hh:mm:ss") self.datetime.setCalendarPopup(True) self.datetime.setDate(QtCore.QDate.currentDate()) - self.datetime_en = QtGui.QCheckBox("Set due date:") + self.datetime.dateTimeChanged.connect(self.enable_duedate) + self.datetime_en = QtGui.QCheckBox("Due date:") grid.addWidget(self.datetime_en, 1, 0) - grid.addWidget(self.datetime, 1, 1, colspan=3) - - self.pipeline = QtGui.QLineEdit() - self.pipeline.insert("main") - grid.addLabel("Pipeline:", 2, 0) - grid.addWidget(self.pipeline, 2, 1) + grid.addWidget(self.datetime, 1, 1) self.priority = QtGui.QSpinBox() self.priority.setRange(-99, 99) - grid.addLabel("Priority:", 2, 2) - grid.addWidget(self.priority, 2, 3) + grid.addWidget(QtGui.QLabel("Priority:"), 1, 2) + grid.addWidget(self.priority, 1, 3) + + self.pipeline = QtGui.QLineEdit() + self.pipeline.insert("main") + grid.addWidget(QtGui.QLabel("Pipeline:"), 2, 0) + grid.addWidget(self.pipeline, 2, 1) + + self.flush = QtGui.QCheckBox("Flush") + grid.addWidget(self.flush, 2, 2, colspan=2) submit = QtGui.QPushButton("Submit") grid.addWidget(submit, 3, 0, colspan=4) submit.clicked.connect(self.submit_clicked) - placeholder = QtGui.QWidget() - splitter.addWidget(placeholder) + self.argsetter = _ArgumentSetter(self.dialog_parent, []) + self.splitter.addWidget(self.argsetter) + self.splitter.setSizes([grid.minimumSizeHint().width(), 1000]) + + def update_argsetter(self, selected, deselected): + selected = selected.indexes() + if selected: + row = selected[0].row() + key = self.explist_model.row_to_key[row] + expinfo = self.explist_model.backing_store[key] + arguments = expinfo["arguments"] + sizes = self.splitter.sizes() + self.argsetter.deleteLater() + self.argsetter = _ArgumentSetter(self.dialog_parent, arguments) + self.splitter.insertWidget(1, self.argsetter) + self.splitter.setSizes(sizes) + + def enable_duedate(self): + self.datetime_en.setChecked(True) @asyncio.coroutine def sub_connect(self, host, port): @@ -78,15 +210,15 @@ class ExplorerDock(dockarea.Dock): return self.explist_model @asyncio.coroutine - def submit(self, pipeline_name, file, experiment, arguments, - priority, due_date): + def submit(self, pipeline_name, file, class_name, arguments, + priority, due_date, flush): expid = { "file": file, - "experiment": experiment, + "class_name": class_name, "arguments": arguments, } rid = yield from self.schedule_ctl.submit(pipeline_name, expid, - priority, due_date) + priority, due_date, flush) self.status_bar.showMessage("Submitted RID {}".format(rid)) def submit_clicked(self): @@ -94,11 +226,15 @@ class ExplorerDock(dockarea.Dock): if idx: row = idx[0].row() key = self.explist_model.row_to_key[row] - expinfo = self.explist_model.data[key] + expinfo = self.explist_model.backing_store[key] if self.datetime_en.isChecked(): due_date = self.datetime.dateTime().toMSecsSinceEpoch()/1000 else: due_date = None + arguments = self.argsetter.get_argument_values() + if arguments is None: + return asyncio.async(self.submit(self.pipeline.text(), - expinfo["file"], expinfo["experiment"], - dict(), self.priority.value(), due_date)) + expinfo["file"], expinfo["class_name"], + arguments, self.priority.value(), + due_date, self.flush.isChecked())) diff --git a/artiq/gui/icon.png b/artiq/gui/icon.png new file mode 100644 index 000000000..33c071886 Binary files /dev/null and b/artiq/gui/icon.png differ diff --git a/artiq/gui/log.py b/artiq/gui/log.py index ed5c9abb6..7a0fb4894 100644 --- a/artiq/gui/log.py +++ b/artiq/gui/log.py @@ -1,7 +1,57 @@ -from quamash import QtGui +import asyncio + +from quamash import QtGui, QtCore from pyqtgraph import dockarea +from artiq.protocols.sync_struct import Subscriber +from artiq.gui.tools import ListSyncModel + + +class _LogModel(ListSyncModel): + def __init__(self, parent, init): + ListSyncModel.__init__(self, + ["RID", "Message"], + parent, init) + + def convert(self, v, column): + return v[column] + class LogDock(dockarea.Dock): def __init__(self): dockarea.Dock.__init__(self, "Log", size=(1000, 300)) + + self.log = QtGui.QTableView() + self.log.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + self.log.horizontalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) + self.log.setHorizontalScrollMode( + QtGui.QAbstractItemView.ScrollPerPixel) + self.log.setShowGrid(False) + self.log.setTextElideMode(QtCore.Qt.ElideNone) + self.addWidget(self.log) + self.scroll_at_bottom = False + + @asyncio.coroutine + def sub_connect(self, host, port): + self.subscriber = Subscriber("log", self.init_log_model) + yield from self.subscriber.connect(host, port) + + @asyncio.coroutine + def sub_close(self): + yield from self.subscriber.close() + + def rows_inserted_before(self): + scrollbar = self.log.verticalScrollBar() + self.scroll_at_bottom = scrollbar.value() == scrollbar.maximum() + + def rows_inserted_after(self): + if self.scroll_at_bottom: + self.log.scrollToBottom() + + def init_log_model(self, init): + table_model = _LogModel(self.log, init) + self.log.setModel(table_model) + table_model.rowsAboutToBeInserted.connect(self.rows_inserted_before) + table_model.rowsInserted.connect(self.rows_inserted_after) + return table_model diff --git a/artiq/gui/moninj.py b/artiq/gui/moninj.py new file mode 100644 index 000000000..282413cc7 --- /dev/null +++ b/artiq/gui/moninj.py @@ -0,0 +1,300 @@ +import asyncio +import logging +import socket +import struct +from operator import itemgetter + +from quamash import QtGui, QtCore +from pyqtgraph import dockarea + +from artiq.tools import TaskObject +from artiq.protocols.sync_struct import Subscriber + + +logger = logging.getLogger(__name__) + + +_mode_enc = { + "exp": 0, + "1": 1, + "0": 2, + "in": 3 +} + + +class _TTLWidget(QtGui.QFrame): + def __init__(self, send_to_device, channel, force_out, name): + self.send_to_device = send_to_device + self.channel = channel + self.force_out = force_out + + QtGui.QFrame.__init__(self) + + self.setFrameShape(QtGui.QFrame.Panel) + self.setFrameShadow(QtGui.QFrame.Raised) + + grid = QtGui.QGridLayout() + self.setLayout(grid) + label = QtGui.QLabel(name) + label.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(label, 1, 1) + + self._direction = QtGui.QLabel() + self._direction.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(self._direction, 2, 1) + self._override = QtGui.QLabel() + self._override.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(self._override, 3, 1) + self._value = QtGui.QLabel() + self._value.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(self._value, 4, 1, 6, 1) + + self._value.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + menu = QtGui.QActionGroup(self._value) + menu.setExclusive(True) + self._expctl_action = QtGui.QAction("Experiment controlled", self._value) + self._expctl_action.setCheckable(True) + menu.addAction(self._expctl_action) + self._value.addAction(self._expctl_action) + self._expctl_action.triggered.connect(lambda: self.set_mode("exp")) + separator = QtGui.QAction(self._value) + separator.setSeparator(True) + self._value.addAction(separator) + self._force1_action = QtGui.QAction("Force 1", self._value) + self._force1_action.setCheckable(True) + menu.addAction(self._force1_action) + self._value.addAction(self._force1_action) + self._force1_action.triggered.connect(lambda: self.set_mode("1")) + self._force0_action = QtGui.QAction("Force 0", self._value) + self._force0_action.setCheckable(True) + menu.addAction(self._force0_action) + self._value.addAction(self._force0_action) + self._force0_action.triggered.connect(lambda: self.set_mode("0")) + self._forcein_action = QtGui.QAction("Force input", self._value) + self._forcein_action.setCheckable(True) + self._forcein_action.setEnabled(not force_out) + menu.addAction(self._forcein_action) + self._value.addAction(self._forcein_action) + self._forcein_action.triggered.connect(lambda: self.set_mode("in")) + + self.set_value(0, False, False) + + def set_mode(self, mode): + data = struct.pack("bbb", + 2, # MONINJ_REQ_TTLSET + self.channel, _mode_enc[mode]) + self.send_to_device(data) + + def set_value(self, value, oe, override): + value_s = "1" if value else "0" + if override: + value_s = "" + value_s + "" + color = " color=\"red\"" + self._override.setText("OVERRIDE") + else: + color = "" + self._override.setText("") + self._value.setText("{}".format( + color, value_s)) + oe = oe or self.force_out + direction = "OUT" if oe else "IN" + self._direction.setText("" + direction + "") + if override: + if oe: + if value: + self._force1_action.setChecked(True) + else: + self._force0_action.setChecked(True) + else: + self._forcein_action.setChecked(True) + else: + self._expctl_action.setChecked(True) + + +class _DDSWidget(QtGui.QFrame): + def __init__(self, send_to_device, channel, sysclk, name): + self.send_to_device = send_to_device + self.channel = channel + self.sysclk = sysclk + self.name = name + + QtGui.QFrame.__init__(self) + + self.setFrameShape(QtGui.QFrame.Panel) + self.setFrameShadow(QtGui.QFrame.Raised) + + grid = QtGui.QGridLayout() + self.setLayout(grid) + label = QtGui.QLabel(name) + label.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(label, 1, 1) + + self._value = QtGui.QLabel() + self._value.setAlignment(QtCore.Qt.AlignCenter) + grid.addWidget(self._value, 2, 1, 6, 1) + + self.set_value(0) + + def set_value(self, ftw): + frequency = ftw*self.sysclk/2**32 + self._value.setText("{:.7f} MHz" + .format(float(frequency)/1e6)) + + +class _DeviceManager: + def __init__(self, send_to_device, init): + self.send_to_device = send_to_device + self.ddb = dict() + self.ttl_cb = lambda: None + self.ttl_widgets = dict() + self.dds_cb = lambda: None + self.dds_widgets = dict() + for k, v in init.items(): + self[k] = v + + def __setitem__(self, k, v): + self.ddb[k] = v + if k in self.ttl_widgets: + del self[k] + if not isinstance(v, dict): + return + try: + if v["type"] == "local": + if v["module"] == "artiq.coredevice.ttl": + channel = v["arguments"]["channel"] + force_out = v["class"] == "TTLOut" + self.ttl_widgets[channel] = _TTLWidget( + self.send_to_device, channel, force_out, k) + self.ttl_cb() + if (v["module"] == "artiq.coredevice.dds" + and v["class"] in {"AD9858", "AD9914"}): + channel = v["arguments"]["channel"] + sysclk = v["arguments"]["sysclk"] + self.dds_widgets[channel] = _DDSWidget( + self.send_to_device, channel, sysclk, k) + self.dds_cb() + except KeyError: + pass + + def __delitem__(self, k): + del self.ddb[k] + if k in self.ttl_widgets: + del self.ttl_widgets[k] + self.ttl_cb() + + def get_core_addr(self): + try: + comm = self.ddb["comm"] + while isinstance(comm, str): + comm = self.ddb[comm] + return comm["arguments"]["host"] + except KeyError: + return None + + +class _MonInjDock(dockarea.Dock): + def __init__(self, name): + dockarea.Dock.__init__(self, name, size=(1500, 500)) + + self.grid = QtGui.QGridLayout() + gridw = QtGui.QWidget() + gridw.setLayout(self.grid) + self.addWidget(gridw) + + def layout_widgets(self, widgets): + w = self.grid.itemAt(0) + while w is not None: + self.grid.removeItem(w) + w = self.grid.itemAt(0) + for i, (_, w) in enumerate(sorted(widgets, key=itemgetter(0))): + self.grid.addWidget(w, i // 4, i % 4) + + +class MonInj(TaskObject): + def __init__(self): + self.ttl_dock = _MonInjDock("TTL") + self.dds_dock = _MonInjDock("DDS") + + self.subscriber = Subscriber("devices", self.init_devices) + self.dm = _DeviceManager(self.send_to_device, dict()) + self.transport = None + + @asyncio.coroutine + def start(self, server, port): + loop = asyncio.get_event_loop() + yield from loop.create_datagram_endpoint(lambda: self, + family=socket.AF_INET) + try: + yield from self.subscriber.connect(server, port) + try: + TaskObject.start(self) + except: + yield from self.subscriber.close() + raise + except: + self.transport.close() + raise + + @asyncio.coroutine + def stop(self): + yield from TaskObject.stop(self) + yield from self.subscriber.close() + if self.transport is not None: + self.transport.close() + self.transport = None + + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + try: + ttl_levels, ttl_oes, ttl_overrides = \ + struct.unpack(">QQQ", data[:8*3]) + for channel, w in self.dm.ttl_widgets.items(): + w.set_value(ttl_levels & (1 << channel), + ttl_oes & (1 << channel), + ttl_overrides & (1 << channel)) + dds_data = data[8*3:] + ndds = len(dds_data)//4 + ftws = struct.unpack(">" + "I"*ndds, dds_data) + for channel, w in self.dm.dds_widgets.items(): + try: + ftw = ftws[channel] + except KeyError: + pass + else: + w.set_value(ftw) + except: + logger.warning("failed to process datagram", exc_info=True) + + def error_received(self, exc): + logger.warning("datagram endpoint error") + + def connection_lost(self, exc): + self.transport = None + + def send_to_device(self, data): + ca = self.dm.get_core_addr() + if ca is None: + logger.warning("could not find core device address") + elif self.transport is None: + logger.warning("datagram endpoint not available") + else: + self.transport.sendto(data, (ca, 3250)) + + @asyncio.coroutine + def _do(self): + while True: + yield from asyncio.sleep(0.2) + # MONINJ_REQ_MONITOR + self.send_to_device(b"\x01") + + def init_devices(self, d): + self.dm = _DeviceManager(self.send_to_device, d) + self.dm.ttl_cb = lambda: self.ttl_dock.layout_widgets( + self.dm.ttl_widgets.items()) + self.dm.dds_cb = lambda: self.dds_dock.layout_widgets( + self.dm.dds_widgets.items()) + self.dm.ttl_cb() + self.dm.dds_cb() + return self.dm diff --git a/artiq/gui/parameters.py b/artiq/gui/parameters.py index c23bbeb6e..5fbac5866 100644 --- a/artiq/gui/parameters.py +++ b/artiq/gui/parameters.py @@ -1,10 +1,11 @@ import asyncio -from quamash import QtGui +from quamash import QtGui, QtCore from pyqtgraph import dockarea +from pyqtgraph import LayoutWidget from artiq.protocols.sync_struct import Subscriber -from artiq.gui.tools import DictSyncModel +from artiq.gui.tools import DictSyncModel, short_format class ParametersModel(DictSyncModel): @@ -19,18 +20,41 @@ class ParametersModel(DictSyncModel): if column == 0: return k elif column == 1: - return str(v) + return short_format(v) else: raise ValueError class ParametersDock(dockarea.Dock): def __init__(self): - dockarea.Dock.__init__(self, "Parameters", size=(500, 300)) + dockarea.Dock.__init__(self, "Parameters", size=(400, 300)) + + grid = LayoutWidget() + self.addWidget(grid) + + self.search = QtGui.QLineEdit() + self.search.setPlaceholderText("search...") + self.search.editingFinished.connect(self.search_parameters) + grid.addWidget(self.search, 0, 0) self.table = QtGui.QTableView() self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) - self.addWidget(self.table) + self.table.horizontalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) + grid.addWidget(self.table, 1, 0) + + def search_parameters(self): + model = self.table.model() + parentIndex = model.index(0, 0) + numRows = model.rowCount(parentIndex) + + for row in range(numRows): + index = model.index(row, 0) + parameter = model.data(index, QtCore.Qt.DisplayRole) + if parameter.startswith(self.search.displayText()): + self.table.showRow(row) + else: + self.table.hideRow(row) @asyncio.coroutine def sub_connect(self, host, port): diff --git a/artiq/gui/results.py b/artiq/gui/results.py new file mode 100644 index 000000000..964856744 --- /dev/null +++ b/artiq/gui/results.py @@ -0,0 +1,109 @@ +import asyncio +from collections import OrderedDict +from functools import partial + +from quamash import QtGui, QtCore +from pyqtgraph import dockarea +from pyqtgraph import LayoutWidget + +from artiq.protocols.sync_struct import Subscriber +from artiq.gui.tools import DictSyncModel, short_format +from artiq.gui.displays import * + + +class ResultsModel(DictSyncModel): + def __init__(self, parent, init): + DictSyncModel.__init__(self, ["Result", "Value"], + parent, init) + + def sort_key(self, k, v): + return k + + def convert(self, k, v, column): + if column == 0: + return k + elif column == 1: + return short_format(v) + else: + raise ValueError + + +class ResultsDock(dockarea.Dock): + def __init__(self, dialog_parent, dock_area): + dockarea.Dock.__init__(self, "Results", size=(1500, 500)) + self.dialog_parent = dialog_parent + self.dock_area = dock_area + + grid = LayoutWidget() + self.addWidget(grid) + + self.table = QtGui.QTableView() + self.table.setSelectionMode(QtGui.QAbstractItemView.NoSelection) + self.table.horizontalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) + grid.addWidget(self.table, 0, 0) + + add_display_box = QtGui.QGroupBox("Add display") + grid.addWidget(add_display_box, 0, 1) + display_grid = QtGui.QGridLayout() + add_display_box.setLayout(display_grid) + + for n, name in enumerate(display_types.keys()): + btn = QtGui.QPushButton(name) + display_grid.addWidget(btn, n, 0) + btn.clicked.connect(partial(self.create_dialog, name)) + + self.displays = dict() + + @asyncio.coroutine + def sub_connect(self, host, port): + self.subscriber = Subscriber("rt_results", self.init_results_model, + self.on_mod) + yield from self.subscriber.connect(host, port) + + @asyncio.coroutine + def sub_close(self): + yield from self.subscriber.close() + + def init_results_model(self, init): + self.table_model = ResultsModel(self.table, init) + self.table.setModel(self.table_model) + return self.table_model + + def on_mod(self, mod): + if mod["action"] == "init": + for display in self.displays.values(): + display.update_data(self.table_model.backing_store) + return + + if mod["action"] == "setitem": + source = mod["key"] + elif mod["path"]: + source = mod["path"][0] + else: + return + + for display in self.displays.values(): + if source in display.data_sources(): + display.update_data(self.table_model.backing_store) + + def create_dialog(self, ty): + dlg_class = display_types[ty][0] + dlg = dlg_class(self.dialog_parent, None, dict(), + sorted(self.table_model.backing_store.keys()), + partial(self.create_display, ty, None)) + dlg.open() + + def create_display(self, ty, prev_name, name, settings): + if prev_name is not None and prev_name in self.displays: + raise NotImplementedError + dsp_class = display_types[ty][1] + dsp = dsp_class(name, settings) + self.displays[name] = dsp + dsp.update_data(self.table_model.backing_store) + + def on_close(): + del self.displays[name] + dsp.sigClosed.connect(on_close) + self.dock_area.addDock(dsp) + self.dock_area.floatDock(dsp) diff --git a/artiq/gui/scan.py b/artiq/gui/scan.py new file mode 100644 index 000000000..1ec7c2dcf --- /dev/null +++ b/artiq/gui/scan.py @@ -0,0 +1,137 @@ +from quamash import QtGui +from pyqtgraph import LayoutWidget + + +class _Range(LayoutWidget): + def __init__(self, global_min, global_max, global_step, unit): + LayoutWidget.__init__(self) + + def apply_properties(spinbox): + if global_min is not None: + spinbox.setMinimum(global_min) + if global_max is not None: + spinbox.setMaximum(global_max) + if global_step is not None: + spinbox.setSingleStep(global_step) + if unit: + spinbox.setSuffix(" " + unit) + + self.addWidget(QtGui.QLabel("Min:"), 0, 0) + self.min = QtGui.QDoubleSpinBox() + apply_properties(self.min) + self.addWidget(self.min, 0, 1) + + self.addWidget(QtGui.QLabel("Max:"), 0, 2) + self.max = QtGui.QDoubleSpinBox() + apply_properties(self.max) + self.addWidget(self.max, 0, 3) + + self.addWidget(QtGui.QLabel("#Points:"), 0, 4) + self.npoints = QtGui.QSpinBox() + self.npoints.setMinimum(2) + self.npoints.setValue(10) + self.addWidget(self.npoints, 0, 5) + + def set_values(self, min, max, npoints): + self.min.setValue(min) + self.max.setValue(max) + self.npoints.setValue(npoints) + + def get_values(self): + return { + "min": self.min.value(), + "max": self.max.value(), + "npoints": self.npoints.value() + } + + +class ScanController(LayoutWidget): + def __init__(self, procdesc): + LayoutWidget.__init__(self) + + self.stack = QtGui.QStackedWidget() + self.addWidget(self.stack, 1, 0, colspan=4) + + gmin, gmax = procdesc["global_min"], procdesc["global_max"] + gstep = procdesc["global_step"] + unit = procdesc["unit"] + + self.v_noscan = QtGui.QDoubleSpinBox() + if gmin is not None: + self.v_noscan.setMinimum(gmin) + if gmax is not None: + self.v_noscan.setMaximum(gmax) + if gstep is not None: + self.v_noscan.setSingleStep(gstep) + if unit: + self.v_noscan.setSuffix(" " + unit) + self.v_noscan_gr = LayoutWidget() + self.v_noscan_gr.addWidget(QtGui.QLabel("Value:"), 0, 0) + self.v_noscan_gr.addWidget(self.v_noscan, 0, 1) + self.stack.addWidget(self.v_noscan_gr) + + self.v_linear = _Range(gmin, gmax, gstep, unit) + self.stack.addWidget(self.v_linear) + + self.v_random = _Range(gmin, gmax, gstep, unit) + self.stack.addWidget(self.v_random) + + self.v_explicit = QtGui.QLineEdit() + self.v_explicit_gr = LayoutWidget() + self.v_explicit_gr.addWidget(QtGui.QLabel("Sequence:"), 0, 0) + self.v_explicit_gr.addWidget(self.v_explicit, 0, 1) + self.stack.addWidget(self.v_explicit_gr) + + self.noscan = QtGui.QRadioButton("No scan") + self.linear = QtGui.QRadioButton("Linear") + self.random = QtGui.QRadioButton("Random") + self.explicit = QtGui.QRadioButton("Explicit") + radiobuttons = QtGui.QButtonGroup() + for n, b in enumerate([self.noscan, self.linear, + self.random, self.explicit]): + self.addWidget(b, 0, n) + radiobuttons.addButton(b) + b.toggled.connect(self.select_page) + + if "default" in procdesc: + d = procdesc["default"] + if d["ty"] == "NoScan": + self.noscan.setChecked(True) + self.v_noscan.setValue(d["value"]) + elif d["ty"] == "LinearScan": + self.linear.setChecked(True) + self.v_linear.set_values(d["min"], d["max"], d["step"]) + elif d["ty"] == "RandomScan": + self.random.setChecked(True) + self.v_random.set_values(d["min"], d["max"], d["step"]) + elif d["ty"] == "ExplicitScan": + self.explicit.setChecked(True) + self.v_explicit.insert(" ".join( + [str(x) for x in d["sequence"]])) + else: + self.noscan.setChecked(True) + + def select_page(self): + if self.noscan.isChecked(): + self.stack.setCurrentWidget(self.v_noscan_gr) + elif self.linear.isChecked(): + self.stack.setCurrentWidget(self.v_linear) + elif self.random.isChecked(): + self.stack.setCurrentWidget(self.v_random) + elif self.explicit.isChecked(): + self.stack.setCurrentWidget(self.v_explicit_gr) + + def get_argument_value(self): + if self.noscan.isChecked(): + return {"ty": "NoScan", "value": self.v_noscan.value()} + elif self.linear.isChecked(): + d = {"ty": "LinearScan"} + d.update(self.v_linear.get_values()) + return d + elif self.random.isChecked(): + d = {"ty": "RandomScan"} + d.update(self.v_random.get_values()) + return d + elif self.explicit.isChecked(): + sequence = [float(x) for x in self.v_explicit.text().split()] + return {"ty": "ExplicitScan", "sequence": sequence} diff --git a/artiq/gui/schedule.py b/artiq/gui/schedule.py index f8cbb9d36..d023c880b 100644 --- a/artiq/gui/schedule.py +++ b/artiq/gui/schedule.py @@ -6,19 +6,18 @@ from pyqtgraph import dockarea from artiq.protocols.sync_struct import Subscriber from artiq.gui.tools import DictSyncModel -from artiq.tools import format_arguments class _ScheduleModel(DictSyncModel): def __init__(self, parent, init): DictSyncModel.__init__(self, ["RID", "Pipeline", "Status", "Prio", "Due date", - "File", "Experiment", "Arguments"], + "File", "Class name"], parent, init) def sort_key(self, k, v): - # order by due date, and then by priority and RID - return (v["due_date"] or 0, -v["priority"], k) + # order by priority, and then by due date and RID + return (-v["priority"], v["due_date"] or 0, k) def convert(self, k, v, column): if column == 0: @@ -38,12 +37,10 @@ class _ScheduleModel(DictSyncModel): elif column == 5: return v["expid"]["file"] elif column == 6: - if v["expid"]["experiment"] is None: + if v["expid"]["class_name"] is None: return "" else: - return v["expid"]["experiment"] - elif column == 7: - return format_arguments(v["expid"]["arguments"]) + return v["expid"]["class_name"] else: raise ValueError @@ -56,6 +53,9 @@ class ScheduleDock(dockarea.Dock): self.table = QtGui.QTableView() self.table.setSelectionBehavior(QtGui.QAbstractItemView.SelectRows) + self.table.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + self.table.horizontalHeader().setResizeMode( + QtGui.QHeaderView.ResizeToContents) self.addWidget(self.table) self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) diff --git a/artiq/gui/tools.py b/artiq/gui/tools.py index 99af51dca..cab4e256d 100644 --- a/artiq/gui/tools.py +++ b/artiq/gui/tools.py @@ -1,29 +1,56 @@ from quamash import QtCore -class _DictSyncSubstruct: +def short_format(v): + t = type(v) + if t is int or t is float: + return str(v) + else: + r = t.__name__ + if t is list or t is dict or t is set: + r += " ({})".format(len(v)) + return r + + +class _SyncSubstruct: def __init__(self, update_cb, ref): self.update_cb = update_cb self.ref = ref - def __getitem__(self, key): - return _DictSyncSubstruct(self.update_cb, self.ref[key]) + def append(self, x): + self.ref.append(x) + self.update_cb() + + def insert(self, i, x): + self.ref.insert(i, x) + self.update_cb() + + def pop(self, i=-1): + self.ref.pop(i) + self.update_cb() def __setitem__(self, key, value): self.ref[key] = value self.update_cb() + def __delitem__(self, key): + self.ref.__delitem__(key) + self.update_cb() + + def __getitem__(self, key): + return _SyncSubstruct(self.update_cb, self.ref[key]) + class DictSyncModel(QtCore.QAbstractTableModel): def __init__(self, headers, parent, init): self.headers = headers - self.data = init - self.row_to_key = sorted(self.data.keys(), - key=lambda k: self.sort_key(k, self.data[k])) + self.backing_store = init + self.row_to_key = sorted(self.backing_store.keys(), + key=lambda k: self.sort_key(k, self.backing_store[k])) QtCore.QAbstractTableModel.__init__(self, parent) def rowCount(self, parent): - return len(self.data) + return len(self.backing_store) def columnCount(self, parent): return len(self.headers) @@ -34,7 +61,7 @@ class DictSyncModel(QtCore.QAbstractTableModel): elif role != QtCore.Qt.DisplayRole: return None k = self.row_to_key[index.row()] - return self.convert(k, self.data[k], index.column()) + return self.convert(k, self.backing_store[k], index.column()) def headerData(self, col, orientation, role): if (orientation == QtCore.Qt.Horizontal @@ -48,7 +75,7 @@ class DictSyncModel(QtCore.QAbstractTableModel): while lo < hi: mid = (lo + hi)//2 if (self.sort_key(self.row_to_key[mid], - self.data[self.row_to_key[mid]]) + self.backing_store[self.row_to_key[mid]]) < self.sort_key(k, v)): lo = mid + 1 else: @@ -56,7 +83,7 @@ class DictSyncModel(QtCore.QAbstractTableModel): return lo def __setitem__(self, k, v): - if k in self.data: + if k in self.backing_store: old_row = self.row_to_key.index(k) new_row = self._find_row(k, v) if old_row == new_row: @@ -65,7 +92,7 @@ class DictSyncModel(QtCore.QAbstractTableModel): else: self.beginMoveRows(QtCore.QModelIndex(), old_row, old_row, QtCore.QModelIndex(), new_row) - self.data[k] = v + self.backing_store[k] = v self.row_to_key[old_row], self.row_to_key[new_row] = \ self.row_to_key[new_row], self.row_to_key[old_row] if old_row != new_row: @@ -73,7 +100,7 @@ class DictSyncModel(QtCore.QAbstractTableModel): else: row = self._find_row(k, v) self.beginInsertRows(QtCore.QModelIndex(), row, row) - self.data[k] = v + self.backing_store[k] = v self.row_to_key.insert(row, k) self.endInsertRows() @@ -81,16 +108,66 @@ class DictSyncModel(QtCore.QAbstractTableModel): row = self.row_to_key.index(k) self.beginRemoveRows(QtCore.QModelIndex(), row, row) del self.row_to_key[row] - del self.data[k] + del self.backing_store[k] self.endRemoveRows() - def __getitem__(self, key): + def __getitem__(self, k): def update(): - self[key] = self.data[key] - return _DictSyncSubstruct(update, self.data[key]) + self[k] = self.backing_store[k] + return _SyncSubstruct(update, self.backing_store[k]) def sort_key(self, k, v): raise NotImplementedError def convert(self, k, v, column): raise NotImplementedError + + +class ListSyncModel(QtCore.QAbstractTableModel): + def __init__(self, headers, parent, init): + self.headers = headers + self.backing_store = init + QtCore.QAbstractTableModel.__init__(self, parent) + + def rowCount(self, parent): + return len(self.backing_store) + + def columnCount(self, parent): + return len(self.headers) + + def data(self, index, role): + if not index.isValid(): + return None + elif role != QtCore.Qt.DisplayRole: + return None + return self.convert(self.backing_store[index.row()], index.column()) + + def headerData(self, col, orientation, role): + if (orientation == QtCore.Qt.Horizontal + and role == QtCore.Qt.DisplayRole): + return self.headers[col] + return None + + def __setitem__(self, k, v): + self.dataChanged.emit(self.index(k, 0), + self.index(k, len(self.headers))) + self.backing_store[k] = v + + def __delitem__(self, k): + self.beginRemoveRows(QtCore.QModelIndex(), k, k) + del self.backing_store[k] + self.endRemoveRows() + + def __getitem__(self, k): + def update(): + self[k] = self.backing_store[k] + return _SyncSubstruct(update, self.backing_store[k]) + + def append(self, v): + row = len(self.backing_store) + self.beginInsertRows(QtCore.QModelIndex(), row, row) + self.backing_store.append(v) + self.endInsertRows() + + def convert(self, v, column): + raise NotImplementedError diff --git a/artiq/language/__init__.py b/artiq/language/__init__.py index e69de29bb..6497cda47 100644 --- a/artiq/language/__init__.py +++ b/artiq/language/__init__.py @@ -0,0 +1,12 @@ +from artiq.language import core, environment, units, scan +from artiq.language.core import * +from artiq.language.environment import * +from artiq.language.units import * +from artiq.language.scan import * + + +__all__ = [] +__all__.extend(core.__all__) +__all__.extend(environment.__all__) +__all__.extend(units.__all__) +__all__.extend(scan.__all__) diff --git a/artiq/language/core.py b/artiq/language/core.py index 4d73f324e..a4ca0b883 100644 --- a/artiq/language/core.py +++ b/artiq/language/core.py @@ -2,8 +2,20 @@ Core ARTIQ extensions to the Python language. """ -from collections import namedtuple as _namedtuple -from functools import wraps as _wraps +from collections import namedtuple +from functools import wraps + + +__all__ = ["int64", "round64", "kernel", "portable", + "set_time_manager", "set_syscall_manager", "set_watchdog_factory", + "RuntimeException", "EncodedException"] + +# global namespace for kernels +kernel_globals = ("sequential", "parallel", + "delay_mu", "now_mu", "at_mu", "delay", + "seconds_to_mu", "mu_to_seconds", + "syscall", "watchdog") +__all__.extend(kernel_globals) class int64(int): @@ -28,7 +40,6 @@ class int64(int): True >>> a + b 6 - """ pass @@ -62,12 +73,11 @@ def round64(x): This function is equivalent to ``int64(round(x))`` but, when targeting static compilation, prevents overflow when the rounded value is too large to fit in a 32-bit integer. - """ return int64(round(x)) -_KernelFunctionInfo = _namedtuple("_KernelFunctionInfo", "core_name k_function") +_KernelFunctionInfo = namedtuple("_KernelFunctionInfo", "core_name k_function") def kernel(arg): @@ -88,20 +98,19 @@ def kernel(arg): The decorator takes an optional parameter that defaults to ``core`` and specifies the name of the attribute to use as core device driver. - """ if isinstance(arg, str): def real_decorator(k_function): - @_wraps(k_function) + @wraps(k_function) def run_on_core(exp, *k_args, **k_kwargs): - return getattr(exp, arg).run(k_function, + return getattr(exp, arg).run(k_function, ((exp,) + k_args), k_kwargs) run_on_core.k_function_info = _KernelFunctionInfo( core_name=arg, k_function=k_function) return run_on_core return real_decorator else: - @_wraps(arg) + @wraps(arg) def run_on_core(exp, *k_args, **k_kwargs): return exp.core.run(arg, ((exp,) + k_args), k_kwargs) run_on_core.k_function_info = _KernelFunctionInfo( @@ -117,7 +126,6 @@ def portable(f): host will be executed on the host (no compilation and execution on the core device). A decorated function called from a kernel will be executed on the core device (no RPC). - """ f.k_function_info = _KernelFunctionInfo(core_name="", k_function=f) return f @@ -131,9 +139,10 @@ class _DummyTimeManager: enter_sequential = _not_implemented enter_parallel = _not_implemented exit = _not_implemented + take_time_mu = _not_implemented + get_time_mu = _not_implemented + set_time_mu = _not_implemented take_time = _not_implemented - get_time = _not_implemented - set_time = _not_implemented _time_manager = _DummyTimeManager() @@ -143,7 +152,6 @@ def set_time_manager(time_manager): directly inside the Python interpreter. The time manager responds to the entering and leaving of parallel/sequential blocks, delays, etc. and provides a time-stamped logging facility for events. - """ global _time_manager _time_manager = time_manager @@ -160,17 +168,10 @@ _syscall_manager = _DummySyscallManager() def set_syscall_manager(syscall_manager): """Set the system call manager used for simulating the core device's runtime in the Python interpreter. - """ global _syscall_manager _syscall_manager = syscall_manager -# global namespace for kernels - -kernel_globals = ("sequential", "parallel", - "delay", "now", "at", "time_to_cycles", "cycles_to_time", - "syscall", "watchdog") - class _Sequential: """In a sequential block, statements are executed one after another, with @@ -190,7 +191,6 @@ class _Parallel: The execution time of a parallel block is the execution time of its longest statement. A parallel block may contain sequential blocks, which themselves may contain parallel blocks, etc. - """ def __enter__(self): _time_manager.enter_parallel() @@ -200,50 +200,49 @@ class _Parallel: parallel = _Parallel() -def delay(duration): - """Increases the RTIO time by the given amount. +def delay_mu(duration): + """Increases the RTIO time by the given amount (in machine units).""" + _time_manager.take_time_mu(duration) - """ + +def now_mu(): + """Retrieves the current RTIO time, in machine units.""" + return _time_manager.get_time_mu() + + +def at_mu(time): + """Sets the RTIO time to the specified absolute value, in machine units.""" + _time_manager.set_time_mu(time) + + +def delay(duration): + """Increases the RTIO time by the given amount (in seconds).""" _time_manager.take_time(duration) -def now(): - """Retrieves the current RTIO time, in seconds. +def seconds_to_mu(seconds, core=None): + """Converts seconds to the corresponding number of machine units + (RTIO cycles). - """ - return _time_manager.get_time() - - -def at(time): - """Sets the RTIO time to the specified absolute value. - - """ - _time_manager.set_time(time) - - -def time_to_cycles(time, core=None): - """Converts time to the corresponding number of RTIO cycles. - - :param time: Time (in seconds) to convert. - :param core: Core device for which to perform the conversion. Specify only - when running in the interpreter (not in kernel). - - """ - if core is None: - raise ValueError("Core device must be specified for time conversion") - return round64(time.amount//core.ref_period) - - -def cycles_to_time(cycles, core=None): - """Converts RTIO cycles to the corresponding time. - - :param time: Cycle count to convert. - :param core: Core device for which to perform the conversion. Specify only + :param seconds: time (in seconds) to convert. + :param core: core device for which to perform the conversion. Specify only when running in the interpreter (not in kernel). """ if core is None: raise ValueError("Core device must be specified for time conversion") - return cycles*core.ref_period + return round64(seconds//core.ref_period) + + +def mu_to_seconds(mu, core=None): + """Converts machine units (RTIO cycles) to seconds. + + :param mu: cycle count to convert. + :param core: core device for which to perform the conversion. Specify only + when running in the interpreter (not in kernel). + """ + if core is None: + raise ValueError("Core device must be specified for time conversion") + return mu*core.ref_period def syscall(*args): diff --git a/artiq/language/db.py b/artiq/language/db.py deleted file mode 100644 index 025dc22d2..000000000 --- a/artiq/language/db.py +++ /dev/null @@ -1,130 +0,0 @@ -""" -Connection to device, parameter and result database. -""" - -class _AttributeKind: - pass - - -class Device(_AttributeKind): - """Represents a device for ``AutoDB`` to process.""" - pass - - -class NoDefault: - """Represents the absence of a default value for ``Parameter`` - and ``Argument``. - """ - pass - - -class Parameter(_AttributeKind): - """Represents a parameter (from the database) for ``AutoDB`` - to process. - - :param default: Default value of the parameter to be used if not found - in the database. - """ - def __init__(self, default=NoDefault): - self.default = default - - -class Argument(_AttributeKind): - """Represents an argument (specifiable at instance creation) for - ``AutoDB`` to process. - - :param default: Default value of the argument to be used if not specified - at instance creation. - """ - def __init__(self, default=NoDefault): - self.default = default - - -class Result(_AttributeKind): - """Represents a result for ``AutoDB`` to process.""" - pass - - -class AutoDB: - """Base class to automate device, parameter and result database access. - - Drivers and experiments should in most cases overload this class to - obtain the parameters and devices (including the core device) that they - need, report results, and modify parameters. - - :param dbh: database hub to use. If ``None``, all devices and parameters - must be supplied as keyword arguments, and reporting results and - modifying parameters is not supported. - """ - class DBKeys: - pass - - realtime_results = dict() - - def __init__(self, dbh=None, **kwargs): - self.dbh = dbh - - for k, v in kwargs.items(): - object.__setattr__(self, k, v) - - for k in dir(self.DBKeys): - if k not in self.__dict__: - ak = getattr(self.DBKeys, k) - if isinstance(ak, Argument): - if ak.default is NoDefault: - raise AttributeError( - "No value specified for argument '{}'".format(k)) - object.__setattr__(self, k, ak.default) - elif isinstance(ak, Device): - try: - dev = self.dbh.get_device(k) - except KeyError: - raise KeyError("Device '{}' not found".format(k)) - object.__setattr__(self, k, dev) - self.build() - if self.dbh is not None and self.realtime_results: - self.dbh.add_rt_results(self.realtime_results) - - def __getattr__(self, name): - ak = getattr(self.DBKeys, name) - if isinstance(ak, Parameter): - try: - if self.dbh is None: - raise KeyError - return self.dbh.get_parameter(name) - except KeyError: - if ak.default is not NoDefault: - return ak.default - else: - raise AttributeError("Parameter '{}' not in database" - " and without default value" - .format(name)) - elif isinstance(ak, Result): - try: - return self.dbh.get_result(name) - except KeyError: - raise AttributeError("Result '{}' not found".format(name)) - else: - raise ValueError - - def __setattr__(self, name, value): - try: - ak = getattr(self.DBKeys, name) - except AttributeError: - object.__setattr__(self, name, value) - else: - if isinstance(ak, Parameter): - self.dbh.set_parameter(name, value) - elif isinstance(ak, Result): - self.dbh.set_result(name, value) - else: - raise ValueError - - def build(self): - """This is called by ``__init__`` after the parameter initialization - is done. - - The user may overload this method to complete the object's - initialization with all parameters available. - """ - pass diff --git a/artiq/language/environment.py b/artiq/language/environment.py new file mode 100644 index 000000000..f6da67803 --- /dev/null +++ b/artiq/language/environment.py @@ -0,0 +1,298 @@ +from collections import OrderedDict +from inspect import isclass + + +__all__ = ["NoDefault", + "FreeValue", "BooleanValue", "EnumerationValue", + "NumberValue", "StringValue", + "HasEnvironment", + "Experiment", "EnvExperiment", "is_experiment"] + + +class NoDefault: + """Represents the absence of a default value.""" + pass + + +class DefaultMissing(Exception): + """Raised by the ``default`` method of argument processors when no default + value is available.""" + pass + + +class _SimpleArgProcessor: + def __init__(self, default=NoDefault): + if default is not NoDefault: + self.default_value = default + + def default(self): + if not hasattr(self, "default_value"): + raise DefaultMissing + return self.default_value + + def process(self, x): + return x + + def describe(self): + d = {"ty": self.__class__.__name__} + if hasattr(self, "default_value"): + d["default"] = self.default_value + return d + + +class FreeValue(_SimpleArgProcessor): + """An argument that can be an arbitrary Python value.""" + pass + + +class BooleanValue(_SimpleArgProcessor): + """A boolean argument.""" + pass + + +class EnumerationValue(_SimpleArgProcessor): + """An argument that can take a string value among a predefined set of + values. + + :param choices: A list of string representing the possible values of the + argument. + """ + def __init__(self, choices, default=NoDefault): + _SimpleArgProcessor.__init__(self, default) + assert default is NoDefault or default in choices + self.choices = choices + + def describe(self): + d = _SimpleArgProcessor.describe(self) + d["choices"] = self.choices + return d + + +class NumberValue(_SimpleArgProcessor): + """An argument that can take a numerical value (typically floating point). + + :param unit: A string representing the unit of the value, for user + interface (UI) purposes. + :param step: The step with with the value should be modified by up/down + buttons in a UI. + :param min: The minimum value of the argument. + :param max: The maximum value of the argument. + """ + def __init__(self, default=NoDefault, unit="", step=None, + min=None, max=None): + _SimpleArgProcessor.__init__(self, default) + self.unit = unit + self.step = step + self.min = min + self.max = max + + def describe(self): + d = _SimpleArgProcessor.describe(self) + d["unit"] = self.unit + d["step"] = self.step + d["min"] = self.min + d["max"] = self.max + return d + + +class StringValue(_SimpleArgProcessor): + """A string argument.""" + pass + + +class HasEnvironment: + """Provides methods to manage the environment of an experiment (devices, + parameters, results, arguments).""" + def __init__(self, dmgr=None, pdb=None, rdb=None, *, + param_override=dict(), default_arg_none=False, **kwargs): + self.requested_args = OrderedDict() + + self.__dmgr = dmgr + self.__pdb = pdb + self.__rdb = rdb + self.__param_override = param_override + self.__default_arg_none = default_arg_none + + self.__kwargs = kwargs + self.__in_build = True + self.build() + self.__in_build = False + for key in self.__kwargs.keys(): + if key not in self.requested_args: + raise TypeError("Got unexpected argument: " + key) + del self.__kwargs + + def build(self): + """Must be implemented by the user to request arguments. + + Other initialization steps such as requesting devices and parameters + or initializing real-time results may also be performed here. + + When the repository is scanned, any requested devices and parameters + are set to ``None``.""" + raise NotImplementedError + + def dbs(self): + return self.__dmgr, self.__pdb, self.__rdb + + def get_argument(self, key, processor=None): + """Retrieves and returns the value of an argument. + + :param key: Name of the argument. + :param processor: A description of how to process the argument, such + as instances of ``BooleanValue`` and ``NumberValue``. + """ + if not self.__in_build: + raise TypeError("get_argument() should only " + "be called from build()") + if processor is None: + processor = FreeValue() + self.requested_args[key] = processor + try: + argval = self.__kwargs[key] + except KeyError: + try: + return processor.default() + except DefaultMissing: + if self.__default_arg_none: + return None + else: + raise + return processor.process(argval) + + def attr_argument(self, key, processor=None): + """Sets an argument as attribute. The names of the argument and of the + attribute are the same.""" + setattr(self, key, self.get_argument(key, processor)) + + def get_device(self, key): + """Creates and returns a device driver.""" + if self.__dmgr is None: + raise ValueError("Device manager not present") + return self.__dmgr.get(key) + + def attr_device(self, key): + """Sets a device driver as attribute. The names of the device driver + and of the attribute are the same.""" + setattr(self, key, self.get_device(key)) + + def get_parameter(self, key, default=NoDefault): + """Retrieves and returns a parameter.""" + if self.__pdb is None: + raise ValueError("Parameter database not present") + if key in self.__param_override: + return self.__param_override[key] + try: + return self.__pdb.get(key) + except KeyError: + if default is not NoDefault: + return default + else: + raise + + def attr_parameter(self, key, default=NoDefault): + """Sets a parameter as attribute. The names of the argument and of the + parameter are the same.""" + setattr(self, key, self.get_parameter(key, default)) + + def set_parameter(self, key, value): + """Writes the value of a parameter into the parameter database.""" + if self.__pdb is None: + raise ValueError("Parameter database not present") + self.__pdb.set(key, value) + + def set_result(self, key, value, realtime=False): + """Writes the value of a result. + + :param realtime: Marks the result as real-time, making it immediately + available to clients such as the user interface. Returns a + ``Notifier`` instance that can be used to modify mutable results + (such as lists) and synchronize the modifications with the clients. + """ + if self.__rdb is None: + raise ValueError("Result database not present") + if realtime: + if key in self.__rdb.nrt: + raise ValueError("Result is already non-realtime") + self.__rdb.rt[key] = value + notifier = self.__rdb.rt[key] + notifier.kernel_attr_init = False + return notifier + else: + if key in self.__rdb.rt.read: + raise ValueError("Result is already realtime") + self.__rdb.nrt[key] = value + + def attr_rtresult(self, key, init_value): + """Writes the value of a real-time result and sets the corresponding + ``Notifier`` as attribute. The names of the result and of the + attribute are the same.""" + setattr(self, key, set_result(key, init_value, True)) + + def get_result(self, key): + """Retrieves the value of a result. + + There is no difference between real-time and non-real-time results + (this function does not return ``Notifier`` instances). + """ + if self.__rdb is None: + raise ValueError("Result database not present") + return self.__rdb.get(key) + + +class Experiment: + """Base class for experiments. + + Deriving from this class enables automatic experiment discovery in + Python modules. + """ + def prepare(self): + """Entry point for pre-computing data necessary for running the + experiment. + + Doing such computations outside of ``run`` enables more efficient + scheduling of multiple experiments that need to access the shared + hardware during part of their execution. + + This method must not interact with the hardware. + """ + pass + + def run(self): + """The main entry point of the experiment. + + This method must be overloaded by the user to implement the main + control flow of the experiment. + + This method may interact with the hardware. + + The experiment may call the scheduler's ``pause`` method while in + ``run``. + """ + raise NotImplementedError + + def analyze(self): + """Entry point for analyzing the results of the experiment. + + This method may be overloaded by the user to implement the analysis + phase of the experiment, for example fitting curves. + + Splitting this phase from ``run`` enables tweaking the analysis + algorithm on pre-existing data, and CPU-bound analyses to be run + overlapped with the next experiment in a pipelined manner. + + This method must not interact with the hardware. + """ + pass + + +class EnvExperiment(Experiment, HasEnvironment): + pass + + +def is_experiment(o): + """Checks if a Python object is an instantiable user experiment.""" + return (isclass(o) + and issubclass(o, Experiment) + and o is not Experiment + and o is not EnvExperiment) diff --git a/artiq/language/experiment.py b/artiq/language/experiment.py deleted file mode 100644 index 3473dcba3..000000000 --- a/artiq/language/experiment.py +++ /dev/null @@ -1,40 +0,0 @@ -from inspect import isclass - - -class Experiment: - """Base class for experiments. - - Deriving from this class enables automatic experiment discovery in - Python modules. - """ - def run(self): - """The main entry point of the experiment. - - This method must be overloaded by the user to implement the main - control flow of the experiment. - """ - raise NotImplementedError - - def analyze(self): - """Entry point for analyzing the results of the experiment. - - This method may be overloaded by the user to implement the analysis - phase of the experiment, for example fitting curves. - - Splitting this phase from ``run`` enables tweaking the analysis - algorithm on pre-existing data, and CPU-bound analyses to be run - overlapped with the next experiment in a pipelined manner. - - This method must not interact with the hardware. - """ - pass - - -def has_analyze(experiment): - """Checks if an experiment instance overloaded its ``analyze`` method.""" - return experiment.analyze.__func__ is not Experiment.analyze - - -def is_experiment(o): - """Checks if a Python object is an instantiable experiment.""" - return isclass(o) and issubclass(o, Experiment) and o is not Experiment diff --git a/artiq/language/scan.py b/artiq/language/scan.py new file mode 100644 index 000000000..90867737d --- /dev/null +++ b/artiq/language/scan.py @@ -0,0 +1,114 @@ +from random import Random, shuffle +import inspect + +from artiq.language.core import * +from artiq.language.environment import NoDefault, DefaultMissing + + +__all__ = ["NoScan", "LinearScan", "RandomScan", "ExplicitScan", "Scannable"] + + +class NoScan: + def __init__(self, value): + self.value = value + + @portable + def _gen(self): + yield self.value + + @portable + def __iter__(self): + return self._gen() + + def describe(self): + return {"ty": "NoScan", "value": self.value} + + +class LinearScan: + def __init__(self, min, max, npoints): + self.min = min + self.max = max + self.npoints = npoints + + @portable + def _gen(self): + r = self.max - self.min + d = self.npoints - 1 + for i in range(self.npoints): + yield r*i/d + self.min + + @portable + def __iter__(self): + return self._gen() + + def describe(self): + return {"ty": "LinearScan", + "min": self.min, "max": self.max, "npoints": self.npoints} + + +class RandomScan: + def __init__(self, min, max, npoints, seed=0): + self.sequence = list(LinearScan(min, max, npoints)) + shuffle(self.sequence, Random(seed).random) + + @portable + def __iter__(self): + return iter(self.sequence) + + def describe(self): + return {"ty": "RandomScan", + "min": self.min, "max": self.max, "npoints": self.npoints} + + +class ExplicitScan: + def __init__(self, sequence): + self.sequence = sequence + + @portable + def __iter__(self): + return iter(self.sequence) + + def describe(self): + return {"ty": "ExplicitScan", "sequence": self.sequence} + + +_ty_to_scan = { + "NoScan": NoScan, + "LinearScan": LinearScan, + "RandomScan": RandomScan, + "ExplicitScan": ExplicitScan +} + + +class Scannable: + def __init__(self, global_min=None, global_max=None, global_step=None, + unit="", default=NoDefault): + self.global_min = global_min + self.global_max = global_max + self.global_step = global_step + self.unit = unit + if default is not NoDefault: + self.default_value = default + + def default(self): + if not hasattr(self, "default_value"): + raise DefaultMissing + return self.default_value + + def process(self, x): + cls = _ty_to_scan[x["ty"]] + args = dict() + for arg in inspect.getargspec(cls).args[1:]: + if arg in x: + args[arg] = x[arg] + return cls(**args) + + def describe(self): + d = {"ty": "Scannable"} + d["global_min"] = self.global_min + d["global_max"] = self.global_max + d["global_step"] = self.global_step + d["unit"] = self.unit + if hasattr(self, "default_value"): + d["default"] = self.default_value.describe() + return d diff --git a/artiq/language/units.py b/artiq/language/units.py index fc8b530f6..83f1133a8 100644 --- a/artiq/language/units.py +++ b/artiq/language/units.py @@ -1,266 +1,20 @@ -""" -Definition and management of physical units. - -""" - -from fractions import Fraction as _Fraction - - -class DimensionError(Exception): - """Raised when attempting an operation with incompatible units. - - When targeting the core device, all units are statically managed at - compilation time. Thus, when raised by functions in this module, this - exception cannot be caught in the kernel as it is raised by the compiler - instead. - - """ - pass - - -def mul_dimension(l, r): - """Returns the unit obtained by multiplying unit ``l`` with unit ``r``. - - Raises ``DimensionError`` if the resulting unit is not implemented. - - """ - if l is None: - return r - if r is None: - return l - if {l, r} == {"Hz", "s"}: - return None - raise DimensionError - - -def _rmul_dimension(l, r): - return mul_dimension(r, l) - - -def div_dimension(l, r): - """Returns the unit obtained by dividing unit ``l`` with unit ``r``. - - Raises ``DimensionError`` if the resulting unit is not implemented. - - """ - if l == r: - return None - if r is None: - return l - if l is None: - if r == "s": - return "Hz" - if r == "Hz": - return "s" - raise DimensionError - - -def _rdiv_dimension(l, r): - return div_dimension(r, l) - - -def addsub_dimension(x, y): - """Returns the unit obtained by adding or subtracting unit ``l`` with - unit ``r``. - - Raises ``DimensionError`` if ``l`` and ``r`` are different. - - """ - if x == y: - return x - else: - raise DimensionError - +__all__ = [] _prefixes_str = "pnum_kMG" -_smallest_prefix = _Fraction(1, 10**12) - - -def _format(amount, unit): - if amount is NotImplemented: - return NotImplemented - if unit is None: - return amount - else: - return Quantity(amount, unit) - - -class Quantity: - """Represents an amount in a given fundamental unit (identified by a - string). - - The amount can be of any Python numerical type (integer, float, - Fraction, ...). - Arithmetic operations and comparisons are directly delegated to the - underlying numerical types. - - """ - def __init__(self, amount, unit): - self.amount = amount - self.unit = unit - - def __repr__(self): - r_amount = self.amount - if isinstance(r_amount, int) or isinstance(r_amount, _Fraction): - r_prefix = 0 - r_amount = r_amount/_smallest_prefix - if r_amount: - numerator = r_amount.numerator - while numerator % 1000 == 0 and r_prefix < len(_prefixes_str): - numerator /= 1000 - r_amount /= 1000 - r_prefix += 1 - prefix_str = _prefixes_str[r_prefix] - if prefix_str == "_": - prefix_str = "" - return str(r_amount) + " " + prefix_str + self.unit - else: - return str(r_amount) + " " + self.unit - - def __float__(self): - return float(self.amount) - - # mul/div - def _binop(self, other, opf_name, dim_function): - opf = getattr(self.amount, opf_name) - if isinstance(other, Quantity): - amount = opf(other.amount) - unit = dim_function(self.unit, other.unit) - else: - amount = opf(other) - unit = dim_function(self.unit, None) - return _format(amount, unit) - - def __mul__(self, other): - return self._binop(other, "__mul__", mul_dimension) - - def __rmul__(self, other): - return self._binop(other, "__rmul__", _rmul_dimension) - - def __truediv__(self, other): - return self._binop(other, "__truediv__", div_dimension) - - def __rtruediv__(self, other): - return self._binop(other, "__rtruediv__", _rdiv_dimension) - - def __floordiv__(self, other): - return self._binop(other, "__floordiv__", div_dimension) - - def __rfloordiv__(self, other): - return self._binop(other, "__rfloordiv__", _rdiv_dimension) - - # unary ops - def __neg__(self): - return Quantity(self.amount.__neg__(), self.unit) - - def __pos__(self): - return Quantity(self.amount.__pos__(), self.unit) - - def __abs__(self): - return Quantity(abs(self.amount), self.unit) - - # add/sub - def __add__(self, other): - return self._binop(other, "__add__", addsub_dimension) - - def __radd__(self, other): - return self._binop(other, "__radd__", addsub_dimension) - - def __sub__(self, other): - return self._binop(other, "__sub__", addsub_dimension) - - def __rsub__(self, other): - return self._binop(other, "__rsub__", addsub_dimension) - - def __mod__(self, other): - return self._binop(other, "__mod__", addsub_dimension) - - def __rmod__(self, other): - return self._binop(other, "__rmod__", addsub_dimension) - - # comparisons - def _cmp(self, other, opf_name): - if not isinstance(other, Quantity) or other.unit != self.unit: - raise DimensionError - return getattr(self.amount, opf_name)(other.amount) - - def __lt__(self, other): - return self._cmp(other, "__lt__") - - def __le__(self, other): - return self._cmp(other, "__le__") - - def __eq__(self, other): - return self._cmp(other, "__eq__") - - def __ne__(self, other): - return self._cmp(other, "__ne__") - - def __gt__(self, other): - return self._cmp(other, "__gt__") - - def __ge__(self, other): - return self._cmp(other, "__ge__") +_smallest_prefix_exp = -12 def _register_unit(unit, prefixes): - amount = _smallest_prefix + exponent = _smallest_prefix_exp for prefix in _prefixes_str: if prefix in prefixes: - quantity = Quantity(amount, unit) full_name = prefix + unit if prefix != "_" else unit - globals()[full_name] = quantity - amount *= 1000 + globals()[full_name] = 10.**exponent + __all__.append(full_name) + exponent += 3 + _register_unit("s", "pnum_") _register_unit("Hz", "_kMG") _register_unit("dB", "_") _register_unit("V", "um_k") - - -def check_unit(value, unit): - """Checks that the value has the specified unit. Unit specification is - a string representing the unit without any prefix (e.g. ``s``, ``Hz``). - Checking for a dimensionless value (not a ``Quantity`` instance) is done - by setting ``unit`` to ``None``. - - If the units do not match, ``DimensionError`` is raised. - - This function can be used in kernels and is executed at compilation time. - - There is already unit checking built into the arithmetic, so you typically - need to use this function only when using the ``amount`` property of - ``Quantity``. - - """ - if unit is None: - if isinstance(value, Quantity): - raise DimensionError - else: - if not isinstance(value, Quantity) or value.unit != unit: - raise DimensionError - -def strip_unit(value, unit): - """Check that the value has the specified unit and returns its amount. - Raises ``DimensionError`` if the units does not match. - - If the passed value is not a ``Quantity``, it is assumed to be in the - specified unit and is returned unchanged. - - If ``unit`` is ``None``, passing a ``Quantity`` as value raises - ``DimensionError``. - - """ - if unit is None: - if isinstance(value, Quantity): - raise DimensionError - else: - return value - else: - if isinstance(value, Quantity): - if value.unit != unit: - raise DimensionError - else: - return value.amount - else: - return value diff --git a/artiq/master/repository.py b/artiq/master/repository.py index 90ef186e9..465ce6f85 100644 --- a/artiq/master/repository.py +++ b/artiq/master/repository.py @@ -1,39 +1,70 @@ import os +import logging +import asyncio from artiq.protocols.sync_struct import Notifier -from artiq.tools import file_import -from artiq.language.experiment import is_experiment +from artiq.master.worker import Worker -def scan_experiments(): +logger = logging.getLogger(__name__) + + +@asyncio.coroutine +def _scan_experiments(log): r = dict() for f in os.listdir("repository"): if f.endswith(".py"): try: - m = file_import(os.path.join("repository", f)) - except: - continue - for k, v in m.__dict__.items(): - if is_experiment(v): - if v.__doc__ is None: - name = k - else: - name = v.__doc__.splitlines()[0].strip() - if name[-1] == ".": - name = name[:-1] + full_name = os.path.join("repository", f) + worker = Worker({"log": lambda message: log("scan", message)}) + try: + description = yield from worker.examine(full_name) + finally: + yield from worker.close() + for class_name, class_desc in description.items(): + name = class_desc["name"] + arguments = class_desc["arguments"] + if name in r: + logger.warning("Duplicate experiment name: '%s'", name) + basename = name + i = 1 + while name in r: + name = basename + str(i) + i += 1 entry = { - "file": os.path.join("repository", f), - "experiment": k, - "gui_file": getattr(v, "__artiq_gui_file__", None) + "file": full_name, + "class_name": class_name, + "arguments": arguments } r[name] = entry + except: + logger.warning("Skipping file '%s'", f, exc_info=True) return r -class Repository: - def __init__(self): - self.explist = Notifier(scan_experiments()) +def _sync_explist(target, source): + for k in list(target.read.keys()): + if k not in source: + del target[k] + for k in source.keys(): + if k not in target.read or target.read[k] != source[k]: + target[k] = source[k] - def get_data(self, filename): - with open(os.path.join("repository", filename)) as f: - return f.read() + +class Repository: + def __init__(self, log_fn): + self.explist = Notifier(dict()) + self._scanning = False + self.log_fn = log_fn + + @asyncio.coroutine + def scan(self): + if self._scanning: + return + self._scanning = True + new_explist = yield from _scan_experiments(self.log_fn) + _sync_explist(self.explist, new_explist) + self._scanning = False + + def scan_async(self): + asyncio.async(self.scan()) diff --git a/artiq/master/results.py b/artiq/master/results.py deleted file mode 100644 index 8dc8eaff6..000000000 --- a/artiq/master/results.py +++ /dev/null @@ -1,103 +0,0 @@ -import os -import time -import re - -import numpy -import h5py - -from artiq.protocols.sync_struct import Notifier, process_mod - - -def get_hdf5_output(start_time, rid, name): - dirname = os.path.join("results", - time.strftime("%Y-%m-%d", start_time), - time.strftime("%H-%M", start_time)) - filename = "{:09}-{}.h5".format(rid, name) - os.makedirs(dirname, exist_ok=True) - return h5py.File(os.path.join(dirname, filename), "w") - - -def get_last_rid(): - r = -1 - try: - day_folders = os.listdir("results") - except: - return r - day_folders = filter(lambda x: re.fullmatch('\d\d\d\d-\d\d-\d\d', x), - day_folders) - for df in day_folders: - day_path = os.path.join("results", df) - try: - minute_folders = os.listdir(day_path) - except: - continue - minute_folders = filter(lambda x: re.fullmatch('\d\d-\d\d', x), - minute_folders) - for mf in minute_folders: - minute_path = os.path.join(day_path, mf) - try: - h5files = os.listdir(minute_path) - except: - continue - for x in h5files: - m = re.fullmatch('(\d\d\d\d\d\d\d\d\d)-.*\.h5', x) - rid = int(m.group(1)) - if rid > r: - r = rid - return r - - -_type_to_hdf5 = { - int: h5py.h5t.STD_I64BE, - float: h5py.h5t.IEEE_F64BE -} - -def result_dict_to_hdf5(f, rd): - for name, data in rd.items(): - if isinstance(data, list): - el_ty = type(data[0]) - for d in data: - if type(d) != el_ty: - raise TypeError("All list elements must have the same" - " type for HDF5 output") - try: - el_ty_h5 = _type_to_hdf5[el_ty] - except KeyError: - raise TypeError("List element type {} is not supported for" - " HDF5 output".format(el_ty)) - dataset = f.create_dataset(name, (len(data), ), el_ty_h5) - dataset[:] = data - elif isinstance(data, numpy.ndarray): - f.create_dataset(name, data=data) - else: - ty = type(data) - try: - ty_h5 = _type_to_hdf5[ty] - except KeyError: - raise TypeError("Type {} is not supported for HDF5 output" - .format(ty)) - dataset = f.create_dataset(name, (), ty_h5) - dataset[()] = data - - -class RTResults: - def __init__(self): - self.groups = Notifier(dict()) - self.current_group = "default" - - def init(self, description): - data = dict() - for rtr in description.keys(): - if isinstance(rtr, tuple): - for e in rtr: - data[e] = [] - else: - data[rtr] = [] - self.groups[self.current_group] = { - "description": description, - "data": data - } - - def update(self, mod): - target = self.groups[self.current_group]["data"] - process_mod(target, mod) diff --git a/artiq/master/scheduler.py b/artiq/master/scheduler.py index 44b14765d..93afb0508 100644 --- a/artiq/master/scheduler.py +++ b/artiq/master/scheduler.py @@ -4,7 +4,8 @@ from enum import Enum from time import time from artiq.master.worker import Worker -from artiq.tools import asyncio_wait_or_cancel, asyncio_queue_peek +from artiq.tools import (asyncio_wait_or_cancel, asyncio_queue_peek, + TaskObject, WaitSet) from artiq.protocols.sync_struct import Notifier @@ -13,27 +14,28 @@ logger = logging.getLogger(__name__) class RunStatus(Enum): pending = 0 - preparing = 1 - prepare_done = 2 - running = 3 - run_done = 4 - analyzing = 5 - analyze_done = 6 - paused = 7 + flushing = 1 + preparing = 2 + prepare_done = 3 + running = 4 + run_done = 5 + analyzing = 6 + analyze_done = 7 + paused = 8 def _mk_worker_method(name): @asyncio.coroutine def worker_method(self, *args, **kwargs): - if self._terminated: + if self.worker.closed.is_set(): return True - m = getattr(self._worker, name) + m = getattr(self.worker, name) try: return (yield from m(*args, **kwargs)) except Exception as e: if isinstance(e, asyncio.CancelledError): raise - if self._terminated: + if self.worker.closed.is_set(): logger.debug("suppressing worker exception of terminated run", exc_info=True) # Return completion on termination @@ -45,7 +47,7 @@ def _mk_worker_method(name): class Run: def __init__(self, rid, pipeline_name, - expid, priority, due_date, + expid, priority, due_date, flush, worker_handlers, notifier): # called through pool self.rid = rid @@ -53,10 +55,11 @@ class Run: self.expid = expid self.priority = priority self.due_date = due_date + self.flush = flush + + self.worker = Worker(worker_handlers) self._status = RunStatus.pending - self._terminated = False - self._worker = Worker(worker_handlers) self._notifier = notifier self._notifier[self.rid] = { @@ -64,6 +67,7 @@ class Run: "expid": self.expid, "priority": self.priority, "due_date": self.due_date, + "flush": self.flush, "status": self._status.name } @@ -74,33 +78,35 @@ class Run: @status.setter def status(self, value): self._status = value - if not self._terminated: + if not self.worker.closed.is_set(): self._notifier[self.rid]["status"] = self._status.name # The run with the largest priority_key is to be scheduled first - def priority_key(self, now): + def priority_key(self, now=None): if self.due_date is None: - overdue = 0 due_date_k = 0 else: - overdue = int(now > self.due_date) due_date_k = -self.due_date - return (overdue, self.priority, due_date_k, -self.rid) + if now is not None and self.due_date is not None: + runnable = int(now > self.due_date) + else: + runnable = 1 + return (runnable, self.priority, due_date_k, -self.rid) @asyncio.coroutine def close(self): # called through pool - self._terminated = True - yield from self._worker.close() + yield from self.worker.close() del self._notifier[self.rid] - _prepare = _mk_worker_method("prepare") + _build = _mk_worker_method("build") @asyncio.coroutine - def prepare(self): - yield from self._prepare(self.rid, self.pipeline_name, self.expid, - self.priority) + def build(self): + yield from self._build(self.rid, self.pipeline_name, self.expid, + self.priority) + prepare = _mk_worker_method("prepare") run = _mk_worker_method("run") resume = _mk_worker_method("resume") analyze = _mk_worker_method("analyze") @@ -120,20 +126,20 @@ class RIDCounter: class RunPool: def __init__(self, ridc, worker_handlers, notifier): self.runs = dict() - self.submitted_callback = None + self.submitted_cb = None self._ridc = ridc self._worker_handlers = worker_handlers self._notifier = notifier - def submit(self, expid, priority, due_date, pipeline_name): + def submit(self, expid, priority, due_date, flush, pipeline_name): # called through scheduler rid = self._ridc.get() - run = Run(rid, pipeline_name, expid, priority, due_date, + run = Run(rid, pipeline_name, expid, priority, due_date, flush, self._worker_handlers, self._notifier) self.runs[rid] = run - if self.submitted_callback is not None: - self.submitted_callback() + if self.submitted_cb is not None: + self.submitted_cb() return rid @asyncio.coroutine @@ -145,29 +151,15 @@ class RunPool: del self.runs[rid] -class TaskObject: - def start(self): - self.task = asyncio.async(self._do()) - - @asyncio.coroutine - def stop(self): - self.task.cancel() - yield from asyncio.wait([self.task]) - del self.task - - @asyncio.coroutine - def _do(self): - raise NotImplementedError - - class PrepareStage(TaskObject): - def __init__(self, deleter, pool, outq): - self.deleter = deleter + def __init__(self, flush_tracker, delete_cb, pool, outq): + self.flush_tracker = flush_tracker + self.delete_cb = delete_cb self.pool = pool self.outq = outq self.pool_submitted = asyncio.Event() - self.pool.submitted_callback = lambda: self.pool_submitted.set() + self.pool.submitted_cb = lambda: self.pool_submitted.set() @asyncio.coroutine def _push_runs(self): @@ -186,14 +178,24 @@ class PrepareStage(TaskObject): # pending_runs is an empty sequence return None if run.due_date is None or run.due_date < now: + if run.flush: + run.status = RunStatus.flushing + yield from asyncio_wait_or_cancel( + [self.flush_tracker.wait_empty(), + run.worker.closed.wait()], + return_when=asyncio.FIRST_COMPLETED) + if run.worker.closed.is_set(): + continue run.status = RunStatus.preparing + self.flush_tracker.add(run.rid) try: + yield from run.build() yield from run.prepare() except: logger.warning("got worker exception in prepare stage, " "deleting RID %d", run.rid, exc_info=True) - self.deleter.delete(run.rid) + self.delete_cb(run.rid) run.status = RunStatus.prepare_done yield from self.outq.put(run) else: @@ -214,8 +216,8 @@ class PrepareStage(TaskObject): class RunStage(TaskObject): - def __init__(self, deleter, inq, outq): - self.deleter = deleter + def __init__(self, delete_cb, inq, outq): + self.delete_cb = delete_cb self.inq = inq self.outq = outq @@ -228,10 +230,9 @@ class RunStage(TaskObject): next_irun = asyncio_queue_peek(self.inq) except asyncio.QueueEmpty: next_irun = None - now = time() if not stack or ( next_irun is not None and - next_irun.priority_key(now) > stack[-1].priority_key(now)): + next_irun.priority_key() > stack[-1].priority_key()): stack.append((yield from self.inq.get())) run = stack.pop() @@ -246,7 +247,7 @@ class RunStage(TaskObject): logger.warning("got worker exception in run stage, " "deleting RID %d", run.rid, exc_info=True) - self.deleter.delete(run.rid) + self.delete_cb(run.rid) else: if completed: run.status = RunStatus.run_done @@ -257,8 +258,8 @@ class RunStage(TaskObject): class AnalyzeStage(TaskObject): - def __init__(self, deleter, inq): - self.deleter = deleter + def __init__(self, delete_cb, inq): + self.delete_cb = delete_cb self.inq = inq @asyncio.coroutine @@ -273,17 +274,23 @@ class AnalyzeStage(TaskObject): logger.warning("got worker exception in analyze stage, " "deleting RID %d", run.rid, exc_info=True) - self.deleter.delete(run.rid) + self.delete_cb(run.rid) run.status = RunStatus.analyze_done - self.deleter.delete(run.rid) + self.delete_cb(run.rid) class Pipeline: def __init__(self, ridc, deleter, worker_handlers, notifier): + flush_tracker = WaitSet() + def delete_cb(rid): + deleter.delete(rid) + flush_tracker.discard(rid) self.pool = RunPool(ridc, worker_handlers, notifier) - self._prepare = PrepareStage(deleter, self.pool, asyncio.Queue(maxsize=1)) - self._run = RunStage(deleter, self._prepare.outq, asyncio.Queue(maxsize=1)) - self._analyze = AnalyzeStage(deleter, self._run.outq) + self._prepare = PrepareStage(flush_tracker, delete_cb, + self.pool, asyncio.Queue(maxsize=1)) + self._run = RunStage(delete_cb, + self._prepare.outq, asyncio.Queue(maxsize=1)) + self._analyze = AnalyzeStage(delete_cb, self._run.outq) def start(self): self._prepare.start() @@ -366,7 +373,7 @@ class Scheduler: if self._pipelines: logger.warning("some pipelines were not garbage-collected") - def submit(self, pipeline_name, expid, priority, due_date): + def submit(self, pipeline_name, expid, priority, due_date, flush): if self._terminated: return try: @@ -377,7 +384,7 @@ class Scheduler: self._worker_handlers, self.notifier) self._pipelines[pipeline_name] = pipeline pipeline.start() - return pipeline.pool.submit(expid, priority, due_date, pipeline_name) + return pipeline.pool.submit(expid, priority, due_date, flush, pipeline_name) def delete(self, rid): self._deleter.delete(rid) diff --git a/artiq/master/worker.py b/artiq/master/worker.py index af7a08990..919906ca2 100644 --- a/artiq/master/worker.py +++ b/artiq/master/worker.py @@ -4,10 +4,11 @@ import logging import subprocess import traceback import time +from functools import partial from artiq.protocols import pyon -from artiq.language.units import strip_unit -from artiq.tools import asyncio_process_wait_timeout, asyncio_wait_or_cancel +from artiq.tools import (asyncio_process_wait_timeout, asyncio_process_wait, + asyncio_wait_or_cancel) logger = logging.getLogger(__name__) @@ -26,14 +27,9 @@ class WorkerError(Exception): class Worker: - def __init__(self, handlers, - send_timeout=0.5, term_timeout=1.0, - prepare_timeout=15.0, results_timeout=15.0): + def __init__(self, handlers=dict(), send_timeout=0.5): self.handlers = handlers self.send_timeout = send_timeout - self.term_timeout = term_timeout - self.prepare_timeout = prepare_timeout - self.results_timeout = results_timeout self.rid = None self.process = None @@ -49,7 +45,7 @@ class Worker: avail = set(range(n_user_watchdogs + 1)) \ - set(self.watchdogs.keys()) wid = next(iter(avail)) - self.watchdogs[wid] = time.monotonic() + strip_unit(t, "s") + self.watchdogs[wid] = time.monotonic() + t return wid def delete_watchdog(self, wid): @@ -74,7 +70,12 @@ class Worker: self.io_lock.release() @asyncio.coroutine - def close(self): + def close(self, term_timeout=1.0): + """Interrupts any I/O with the worker process and terminates the + worker process. + + This method should always be called by the user to clean up, even if + build() or examine() raises an exception.""" self.closed.set() yield from self.io_lock.acquire() try: @@ -83,33 +84,35 @@ class Worker: logger.debug("worker was not created (RID %s)", self.rid) return if self.process.returncode is not None: - logger.debug("worker already terminated (RID %d)", self.rid) + logger.debug("worker already terminated (RID %s)", self.rid) if self.process.returncode != 0: logger.warning("worker finished with status code %d" - " (RID %d)", self.process.returncode, + " (RID %s)", self.process.returncode, self.rid) return obj = {"action": "terminate"} try: - yield from self._send(obj, self.send_timeout, cancellable=False) + yield from self._send(obj, cancellable=False) except: logger.warning("failed to send terminate command to worker" - " (RID %d), killing", self.rid, exc_info=True) + " (RID %s), killing", self.rid, exc_info=True) self.process.kill() + yield from asyncio_process_wait(self.process) return try: yield from asyncio_process_wait_timeout(self.process, - self.term_timeout) + term_timeout) except asyncio.TimeoutError: - logger.warning("worker did not exit (RID %d), killing", self.rid) + logger.warning("worker did not exit (RID %s), killing", self.rid) self.process.kill() + yield from asyncio_process_wait(self.process) else: - logger.debug("worker exited gracefully (RID %d)", self.rid) + logger.debug("worker exited gracefully (RID %s)", self.rid) finally: self.io_lock.release() @asyncio.coroutine - def _send(self, obj, timeout, cancellable=True): + def _send(self, obj, cancellable=True): assert self.io_lock.locked() line = pyon.encode(obj) self.process.stdin.write(line.encode()) @@ -118,7 +121,7 @@ class Worker: if cancellable: ifs.append(self.closed.wait()) fs = yield from asyncio_wait_or_cancel( - ifs, timeout=timeout, + ifs, timeout=self.send_timeout, return_when=asyncio.FIRST_COMPLETED) if all(f.cancelled() for f in fs): raise WorkerTimeout("Timeout sending data to worker") @@ -135,7 +138,7 @@ class Worker: [self.process.stdout.readline(), self.closed.wait()], timeout=timeout, return_when=asyncio.FIRST_COMPLETED) if all(f.cancelled() for f in fs): - raise WorkerTimeout("Timeout sending data to worker") + raise WorkerTimeout("Timeout receiving data from worker") if self.closed.is_set(): raise WorkerError("Data transmission to worker cancelled") line = fs[0].result() @@ -168,8 +171,12 @@ class Worker: func = self.create_watchdog elif action == "delete_watchdog": func = self.delete_watchdog + elif action == "register_experiment": + func = self.register_experiment else: func = self.handlers[action] + if getattr(func, "worker_pass_rid", False): + func = partial(func, self.rid) try: data = func(**obj) reply = {"status": "ok", "data": data} @@ -178,7 +185,7 @@ class Worker: "message": traceback.format_exc()} yield from self.io_lock.acquire() try: - yield from self._send(reply, self.send_timeout) + yield from self._send(reply) finally: self.io_lock.release() @@ -189,7 +196,7 @@ class Worker: try: yield from self.io_lock.acquire() try: - yield from self._send(obj, self.send_timeout) + yield from self._send(obj) finally: self.io_lock.release() try: @@ -202,16 +209,20 @@ class Worker: return completed @asyncio.coroutine - def prepare(self, rid, pipeline_name, expid, priority): + def build(self, rid, pipeline_name, expid, priority, timeout=15.0): self.rid = rid yield from self._create_process() yield from self._worker_action( - {"action": "prepare", + {"action": "build", "rid": rid, "pipeline_name": pipeline_name, "expid": expid, "priority": priority}, - self.prepare_timeout) + timeout) + + @asyncio.coroutine + def prepare(self): + yield from self._worker_action({"action": "prepare"}) @asyncio.coroutine def run(self): @@ -236,6 +247,18 @@ class Worker: yield from self._worker_action({"action": "analyze"}) @asyncio.coroutine - def write_results(self): + def write_results(self, timeout=15.0): yield from self._worker_action({"action": "write_results"}, - self.results_timeout) + timeout) + + @asyncio.coroutine + def examine(self, file, timeout=20.0): + yield from self._create_process() + r = dict() + def register(class_name, name, arguments): + r[class_name] = {"name": name, "arguments": arguments} + self.register_experiment = register + yield from self._worker_action({"action": "examine", + "file": file}, timeout) + del self.register_experiment + return r diff --git a/artiq/master/worker_db.py b/artiq/master/worker_db.py index e3c5865bc..0da07dcf7 100644 --- a/artiq/master/worker_db.py +++ b/artiq/master/worker_db.py @@ -1,76 +1,114 @@ from collections import OrderedDict import importlib import logging +import os +import time +import re + +import numpy +import h5py from artiq.protocols.sync_struct import Notifier from artiq.protocols.pc_rpc import Client, BestEffortClient -from artiq.master.results import result_dict_to_hdf5 logger = logging.getLogger(__name__) -class ResultDB: - def __init__(self, init_rt_results, update_rt_results): - self.init_rt_results = init_rt_results - self.update_rt_results = update_rt_results - self.rtr_description = dict() +def get_hdf5_output(start_time, rid, name): + dirname = os.path.join("results", + time.strftime("%Y-%m-%d", start_time), + time.strftime("%H-%M", start_time)) + filename = "{:09}-{}.h5".format(rid, name) + os.makedirs(dirname, exist_ok=True) + return h5py.File(os.path.join(dirname, filename), "w") - def add_rt_results(self, rtr_description): - intr = set(self.rtr_description.keys()).intersection( - set(rtr_description.keys())) - if intr: - raise ValueError("Duplicate realtime results: " + ", ".join(intr)) - self.rtr_description.update(rtr_description) - def build(self): - realtime_results_set = set() - for rtr in self.rtr_description.keys(): - if isinstance(rtr, tuple): - for e in rtr: - realtime_results_set.add(e) - else: - realtime_results_set.add(rtr) - - self.realtime_data = Notifier({x: [] for x in realtime_results_set}) - self.data = Notifier(dict()) - - self.init_rt_results(self.rtr_description) - self.realtime_data.publish = lambda notifier, data: \ - self.update_rt_results(data) - - def _request(self, name): - try: - return self.realtime_data[name] - except KeyError: - try: - return self.data[name] - except KeyError: - self.data[name] = [] - return self.data[name] - - def request(self, name): - r = self._request(name) - r.kernel_attr_init = False +def get_last_rid(): + r = -1 + try: + day_folders = os.listdir("results") + except: return r + day_folders = filter(lambda x: re.fullmatch('\d\d\d\d-\d\d-\d\d', x), + day_folders) + for df in day_folders: + day_path = os.path.join("results", df) + try: + minute_folders = os.listdir(day_path) + except: + continue + minute_folders = filter(lambda x: re.fullmatch('\d\d-\d\d', x), + minute_folders) + for mf in minute_folders: + minute_path = os.path.join(day_path, mf) + try: + h5files = os.listdir(minute_path) + except: + continue + for x in h5files: + m = re.fullmatch('(\d\d\d\d\d\d\d\d\d)-.*\.h5', x) + rid = int(m.group(1)) + if rid > r: + r = rid + return r - def set(self, name, value): - if name in self.realtime_data.read: - self.realtime_data[name] = value + +_type_to_hdf5 = { + int: h5py.h5t.STD_I64BE, + float: h5py.h5t.IEEE_F64BE +} + +def result_dict_to_hdf5(f, rd): + for name, data in rd.items(): + if isinstance(data, list): + el_ty = type(data[0]) + for d in data: + if type(d) != el_ty: + raise TypeError("All list elements must have the same" + " type for HDF5 output") + try: + el_ty_h5 = _type_to_hdf5[el_ty] + except KeyError: + raise TypeError("List element type {} is not supported for" + " HDF5 output".format(el_ty)) + dataset = f.create_dataset(name, (len(data), ), el_ty_h5) + dataset[:] = data + elif isinstance(data, numpy.ndarray): + f.create_dataset(name, data=data) else: - self.data[name] = value + ty = type(data) + try: + ty_h5 = _type_to_hdf5[ty] + except KeyError: + raise TypeError("Type {} is not supported for HDF5 output" + .format(ty)) + dataset = f.create_dataset(name, (), ty_h5) + dataset[()] = data + + +class ResultDB: + def __init__(self): + self.rt = Notifier(dict()) + self.nrt = dict() + + def get(self, key): + try: + return self.nrt[key] + except KeyError: + return self.rt[key].read def write_hdf5(self, f): - result_dict_to_hdf5(f, self.realtime_data.read) - result_dict_to_hdf5(f, self.data.read) + result_dict_to_hdf5(f, self.rt.read) + result_dict_to_hdf5(f, self.nrt) -def _create_device(desc, dbh): +def _create_device(desc, dmgr): ty = desc["type"] if ty == "local": module = importlib.import_module(desc["module"]) device_class = getattr(module, desc["class"]) - return device_class(dbh, **desc["arguments"]) + return device_class(dmgr, **desc["arguments"]) elif ty == "controller": if desc["best_effort"]: cl = BestEffortClient @@ -81,30 +119,26 @@ def _create_device(desc, dbh): raise ValueError("Unsupported type in device DB: " + ty) -class DBHub: - """Connects device, parameter and result databases to experiment. - Handle device driver creation and destruction. - """ - def __init__(self, ddb, pdb, rdb, read_only=False): +class DeviceManager: + """Handles creation and destruction of local device drivers and controller + RPC clients.""" + def __init__(self, ddb, virtual_devices=dict()): self.ddb = ddb + self.virtual_devices = virtual_devices self.active_devices = OrderedDict() - self.get_parameter = pdb.request - - if not read_only: - self.set_parameter = pdb.set - self.add_rt_results = rdb.add_rt_results - self.get_result = rdb.request - self.set_result = rdb.set - - def get_device(self, name): + def get(self, name): + """Get the device driver or controller client corresponding to a + device database entry.""" + if name in self.virtual_devices: + return self.virtual_devices[name] if name in self.active_devices: return self.active_devices[name] else: - desc = self.ddb.request(name) + desc = self.ddb.get(name) while isinstance(desc, str): # alias - desc = self.ddb.request(desc) + desc = self.ddb.get(desc) dev = _create_device(desc, self) self.active_devices[name] = dev return dev diff --git a/artiq/master/worker_impl.py b/artiq/master/worker_impl.py index 18e0e95ec..77ff1349c 100644 --- a/artiq/master/worker_impl.py +++ b/artiq/master/worker_impl.py @@ -3,9 +3,8 @@ import time from artiq.protocols import pyon from artiq.tools import file_import -from artiq.master.worker_db import DBHub, ResultDB -from artiq.master.results import get_hdf5_output -from artiq.language.experiment import is_experiment +from artiq.master.worker_db import DeviceManager, ResultDB, get_hdf5_output +from artiq.language.environment import is_experiment from artiq.language.core import set_watchdog_factory @@ -45,16 +44,34 @@ def make_parent_action(action, argnames, exception=ParentActionError): return parent_action + + +class LogForwarder: + def __init__(self): + self.buffer = "" + + to_parent = staticmethod(make_parent_action("log", "message")) + + def write(self, data): + self.buffer += data + while "\n" in self.buffer: + i = self.buffer.index("\n") + self.to_parent(self.buffer[:i]) + self.buffer = self.buffer[i+1:] + + def flush(self): + pass + + class ParentDDB: - request = make_parent_action("req_device", "name", KeyError) + get = make_parent_action("get_device", "name", KeyError) class ParentPDB: - request = make_parent_action("req_parameter", "name", KeyError) + get = make_parent_action("get_parameter", "name", KeyError) set = make_parent_action("set_parameter", "name value") -init_rt_results = make_parent_action("init_rt_results", "description") update_rt_results = make_parent_action("update_rt_results", "mod") @@ -79,18 +96,18 @@ class Scheduler: pause = staticmethod(make_parent_action("pause", "")) submit = staticmethod(make_parent_action("scheduler_submit", - "pipeline_name expid priority due_date")) + "pipeline_name expid priority due_date flush")) cancel = staticmethod(make_parent_action("scheduler_cancel", "rid")) - def __init__(self, pipeline_name, expid, priority): + def set_run_info(self, pipeline_name, expid, priority): self.pipeline_name = pipeline_name self.expid = expid self.priority = priority -def get_exp(file, exp): +def get_exp(file, class_name): module = file_import(file) - if exp is None: + if class_name is None: exps = [v for k, v in module.__dict__.items() if is_experiment(v)] if len(exps) != 1: @@ -98,11 +115,44 @@ def get_exp(file, exp): .format(len(exps))) return exps[0] else: - return getattr(module, exp) + return getattr(module, class_name) + + +register_experiment = make_parent_action("register_experiment", + "class_name name arguments") + + +class DummyDMGR: + def get(self, name): + return None + + +class DummyPDB: + def get(self, name): + return None + + def set(self, name, value): + pass + + +def examine(dmgr, pdb, rdb, file): + module = file_import(file) + for class_name, exp_class in module.__dict__.items(): + if is_experiment(exp_class): + if exp_class.__doc__ is None: + name = class_name + else: + name = exp_class.__doc__.splitlines()[0].strip() + if name[-1] == ".": + name = name[:-1] + exp_inst = exp_class(dmgr, pdb, rdb, default_arg_none=True) + arguments = [(k, v.describe()) + for k, v in exp_inst.requested_args.items()] + register_experiment(class_name, name, arguments) def main(): - sys.stdout = sys.stderr + sys.stdout = sys.stderr = LogForwarder() start_time = None rid = None @@ -110,26 +160,27 @@ def main(): exp = None exp_inst = None - rdb = ResultDB(init_rt_results, update_rt_results) - dbh = DBHub(ParentDDB, ParentPDB, rdb) + dmgr = DeviceManager(ParentDDB, + virtual_devices={"scheduler": Scheduler()}) + rdb = ResultDB() + rdb.rt.publish = update_rt_results try: while True: obj = get_object() action = obj["action"] - if action == "prepare": + if action == "build": start_time = time.localtime() rid = obj["rid"] - pipeline_name = obj["pipeline_name"] expid = obj["expid"] - priority = obj["priority"] - exp = get_exp(expid["file"], expid["experiment"]) - exp_inst = exp(dbh, - scheduler=Scheduler(pipeline_name, - expid, - priority), - **expid["arguments"]) - rdb.build() + exp = get_exp(expid["file"], expid["class_name"]) + dmgr.virtual_devices["scheduler"].set_run_info( + obj["pipeline_name"], expid, obj["priority"]) + exp_inst = exp(dmgr, ParentPDB, rdb, + **expid["arguments"]) + put_object({"action": "completed"}) + elif action == "prepare": + exp_inst.prepare() put_object({"action": "completed"}) elif action == "run": exp_inst.run() @@ -144,10 +195,13 @@ def main(): finally: f.close() put_object({"action": "completed"}) + elif action == "examine": + examine(DummyDMGR(), DummyPDB(), ResultDB(), obj["file"]) + put_object({"action": "completed"}) elif action == "terminate": break finally: - dbh.close_devices() + dmgr.close_devices() if __name__ == "__main__": main() diff --git a/artiq/protocols/file_db.py b/artiq/protocols/file_db.py index 71228985c..b7499587e 100644 --- a/artiq/protocols/file_db.py +++ b/artiq/protocols/file_db.py @@ -20,7 +20,7 @@ class FlatFileDB: def save(self): pyon.store_file(self.filename, self.data.read) - def request(self, name): + def get(self, name): return self.data.read[name] def set(self, name, value): @@ -36,19 +36,3 @@ class FlatFileDB: timestamp = time() for hook in self.hooks: hook.delete(timestamp, name) - - -class SimpleHistory: - def __init__(self, depth): - self.depth = depth - self.history = Notifier([]) - - def set(self, timestamp, name, value): - if len(self.history.read) >= self.depth: - del self.history[0] - self.history.append((timestamp, name, value)) - - def delete(self, timestamp, name): - if len(self.history.read) >= self.depth: - del self.history[0] - self.history.append((timestamp, name)) diff --git a/artiq/protocols/fire_and_forget.py b/artiq/protocols/fire_and_forget.py new file mode 100644 index 000000000..556774531 --- /dev/null +++ b/artiq/protocols/fire_and_forget.py @@ -0,0 +1,45 @@ +import threading +import logging + + +logger = logging.getLogger(__name__) + + +class FFProxy: + """Proxies a target object and runs its methods in the background. + + All method calls to this object are forwarded to the target and executed + in a background thread. Method calls return immediately. Exceptions from + the target method are turned into warnings. At most one method from the + target object may be executed in the background; if a new call is + submitted while the previous one is still executing, a warning is printed + and the new call is dropped. + + This feature is typically used to wrap slow and non-critical RPCs in + experiments. + """ + def __init__(self, target): + self.target = target + self._thread = None + + def ff_join(self): + """Waits until any background method finishes its execution.""" + if self._thread is not None: + self._thread.join() + + def __getattr__(self, k): + def run_in_thread(*args, **kwargs): + if self._thread is not None and self._thread.is_alive(): + logger.warning("skipping fire-and-forget call to %r.%s as " + "previous call did not complete", + self.target, k) + return + def thread_body(): + try: + getattr(self.target, k)(*args, **kwargs) + except: + logger.warning("fire-and-forget call to %r.%s raised an " + "exception:", self.target, k, exc_info=True) + self._thread = threading.Thread(target=thread_body) + self._thread.start() + return run_in_thread diff --git a/artiq/protocols/pc_rpc.py b/artiq/protocols/pc_rpc.py index da2c5ae66..a16c3dfd9 100644 --- a/artiq/protocols/pc_rpc.py +++ b/artiq/protocols/pc_rpc.py @@ -22,7 +22,6 @@ import inspect from artiq.protocols import pyon from artiq.protocols.asyncio_server import AsyncioServer as _AsyncioServer -from artiq.tools import format_arguments logger = logging.getLogger(__name__) @@ -246,7 +245,8 @@ class AsyncioClient: def __getattr__(self, name): @asyncio.coroutine def proxy(*args, **kwargs): - return self.__do_rpc(name, args, kwargs) + res = yield from self.__do_rpc(name, args, kwargs) + return res return proxy @@ -374,6 +374,16 @@ class BestEffortClient: return proxy +def _format_arguments(arguments): + fmtargs = [] + for k, v in sorted(arguments.items(), key=itemgetter(0)): + fmtargs.append(k + "=" + repr(v)) + if fmtargs: + return ", ".join(fmtargs) + else: + return "" + + class _PrettyPrintCall: def __init__(self, obj): self.obj = obj @@ -382,7 +392,7 @@ class _PrettyPrintCall: r = self.obj["name"] + "(" args = ", ".join([repr(a) for a in self.obj["args"]]) r += args - kwargs = format_arguments(self.obj["kwargs"]) + kwargs = _format_arguments(self.obj["kwargs"]) if args and kwargs: r += ", " r += kwargs @@ -442,15 +452,18 @@ class Server(_AsyncioServer): try: if obj["action"] == "get_rpc_method_list": members = inspect.getmembers(target, inspect.ismethod) - methods = {} + doc = { + "docstring": inspect.getdoc(target), + "methods": {} + } for name, method in members: if name.startswith("_"): continue method = getattr(target, name) argspec = inspect.getfullargspec(method) - methods[name] = (dict(argspec.__dict__), - inspect.getdoc(method)) - obj = {"status": "ok", "ret": methods} + doc["methods"][name] = (dict(argspec.__dict__), + inspect.getdoc(method)) + obj = {"status": "ok", "ret": doc} elif obj["action"] == "call": logger.debug("calling %s", _PrettyPrintCall(obj)) method = getattr(target, obj["name"]) diff --git a/artiq/protocols/pyon.py b/artiq/protocols/pyon.py index 320c691bf..3998d8bc8 100644 --- a/artiq/protocols/pyon.py +++ b/artiq/protocols/pyon.py @@ -14,7 +14,6 @@ The main rationale for this new custom serializer (instead of using JSON) is that JSON does not support Numpy and more generally cannot be extended with other data types while keeping a concise syntax. Here we can use the Python function call syntax to mark special data types. - """ @@ -25,8 +24,6 @@ import tempfile import numpy -from artiq.language.units import Quantity - _encode_map = { type(None): "none", @@ -39,7 +36,6 @@ _encode_map = { list: "list", dict: "dict", Fraction: "fraction", - Quantity: "quantity", numpy.ndarray: "nparray" } @@ -110,10 +106,6 @@ class _Encoder: return "Fraction({}, {})".format(encode(x.numerator), encode(x.denominator)) - def encode_quantity(self, x): - return "Quantity({}, {})".format(encode(x.amount), - encode(x.unit)) - def encode_nparray(self, x): r = "nparray(" r += encode(x.shape) + ", " @@ -147,7 +139,6 @@ _eval_dict = { "true": True, "Fraction": Fraction, - "Quantity": Quantity, "nparray": _nparray } diff --git a/artiq/protocols/sync_struct.py b/artiq/protocols/sync_struct.py index 95b390faa..e8fd2c69d 100644 --- a/artiq/protocols/sync_struct.py +++ b/artiq/protocols/sync_struct.py @@ -8,11 +8,11 @@ describing each modification made to the structure (*mods*). Structures must be PYON serializable and contain only lists, dicts, and immutable types. Lists and dicts can be nested arbitrarily. - """ import asyncio from operator import getitem +from functools import partial from artiq.protocols import pyon from artiq.protocols.asyncio_server import AsyncioServer @@ -22,9 +22,7 @@ _init_string = b"ARTIQ sync_struct\n" def process_mod(target, mod): - """Apply a *mod* to the target, mutating it. - - """ + """Apply a *mod* to the target, mutating it.""" for key in mod["path"]: target = getitem(target, key) action = mod["action"] @@ -52,8 +50,8 @@ class Subscriber: Multiple functions can be specified in a list for the ``Subscriber`` to update several local objects simultaneously. :param notify_cb: An optional function called every time a mod is received - from the publisher. The mod is passed as parameter. - + from the publisher. The mod is passed as parameter. The function is + called after the mod has been processed. """ def __init__(self, notifier_name, target_builder, notify_cb=None): self.notifier_name = notifier_name @@ -133,7 +131,6 @@ class Notifier: :param backing_struct: Structure to encapsulate. For convenience, it also becomes available as the ``read`` property of the ``Notifier``. - """ def __init__(self, backing_struct, root=None, path=[]): self.read = backing_struct @@ -149,52 +146,46 @@ class Notifier: # All modifications must go through them! def append(self, x): - """Append to a list. - - """ + """Append to a list.""" self._backing_struct.append(x) if self.root.publish is not None: - self.root.publish(self.root, {"action": "append", - "path": self._path, - "x": x}) + self.root.publish({"action": "append", + "path": self._path, + "x": x}) def insert(self, i, x): - """Insert an element into a list. - - """ + """Insert an element into a list.""" self._backing_struct.insert(i, x) if self.root.publish is not None: - self.root.publish(self.root, {"action": "insert", - "path": self._path, - "i": i, "x": x}) + self.root.publish({"action": "insert", + "path": self._path, + "i": i, "x": x}) def pop(self, i=-1): """Pop an element from a list. The returned element is not encapsulated in a ``Notifier`` and its mutations are no longer - tracked. - - """ + tracked.""" r = self._backing_struct.pop(i) if self.root.publish is not None: - self.root.publish(self.root, {"action": "pop", - "path": self._path, - "i": i}) + self.root.publish({"action": "pop", + "path": self._path, + "i": i}) return r def __setitem__(self, key, value): self._backing_struct.__setitem__(key, value) if self.root.publish is not None: - self.root.publish(self.root, {"action": "setitem", - "path": self._path, - "key": key, - "value": value}) + self.root.publish({"action": "setitem", + "path": self._path, + "key": key, + "value": value}) def __delitem__(self, key): self._backing_struct.__delitem__(key) if self.root.publish is not None: - self.root.publish(self.root, {"action": "delitem", - "path": self._path, - "key": key}) + self.root.publish({"action": "delitem", + "path": self._path, + "key": key}) def __getitem__(self, key): item = getitem(self._backing_struct, key) @@ -208,7 +199,6 @@ class Publisher(AsyncioServer): :param notifiers: A dictionary containing the notifiers to associate with the ``Publisher``. The keys of the dictionary are the names of the notifiers to be used with ``Subscriber``. - """ def __init__(self, notifiers): AsyncioServer.__init__(self) @@ -217,7 +207,7 @@ class Publisher(AsyncioServer): self._notifier_names = {id(v): k for k, v in notifiers.items()} for notifier in notifiers.values(): - notifier.publish = self.publish + notifier.publish = partial(self.publish, notifier) @asyncio.coroutine def _handle_connection_cr(self, reader, writer): @@ -256,8 +246,8 @@ class Publisher(AsyncioServer): finally: writer.close() - def publish(self, notifier, obj): - line = pyon.encode(obj) + "\n" + def publish(self, notifier, mod): + line = pyon.encode(mod) + "\n" line = line.encode() notifier_name = self._notifier_names[id(notifier)] for recipient in self._recipients[notifier_name]: diff --git a/artiq/py2llvm_old/__init__.py b/artiq/py2llvm_old/__init__.py index fefb2b9ff..ebb8a93af 100644 --- a/artiq/py2llvm_old/__init__.py +++ b/artiq/py2llvm_old/__init__.py @@ -1,6 +1,6 @@ from artiq.py2llvm.module import Module -def get_runtime_binary(env, func_def): - module = Module(env) +def get_runtime_binary(runtime, func_def): + module = Module(runtime) module.compile_function(func_def, dict()) return module.emit_object() diff --git a/artiq/py2llvm_old/ast_body.py b/artiq/py2llvm_old/ast_body.py index f9c635cb8..b310ae78c 100644 --- a/artiq/py2llvm_old/ast_body.py +++ b/artiq/py2llvm_old/ast_body.py @@ -1,6 +1,6 @@ from pythonparser import ast -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll from artiq.py2llvm import values, base_types, fractions, lists, iterators from artiq.py2llvm.tools import is_terminated @@ -39,8 +39,8 @@ _ast_cmps = { class Visitor: - def __init__(self, env, ns, builder=None): - self.env = env + def __init__(self, runtime, ns, builder=None): + self.runtime = runtime self.ns = ns self.builder = builder self._break_stack = [] @@ -182,7 +182,7 @@ class Visitor: self.builder, [self.visit_expression(arg) for arg in node.args]) elif fn == "syscall": - return self.env.build_syscall( + return self.runtime.build_syscall( node.args[0].s, [self.visit_expression(expr) for expr in node.args[1:]], self.builder) @@ -420,7 +420,7 @@ class Visitor: def _break_loop_body(self, target_block): exception_levels = self._exception_level_stack[-1] if exception_levels: - self.env.build_pop(self.builder, exception_levels) + self.runtime.build_pop(self.builder, exception_levels) self.builder.branch(target_block) def _visit_stmt_Break(self, node): @@ -436,7 +436,7 @@ class Visitor: val = self.visit_expression(node.value) exception_levels = sum(self._exception_level_stack) if exception_levels: - self.env.build_pop(self.builder, exception_levels) + self.runtime.build_pop(self.builder, exception_levels) if isinstance(val, base_types.VNone): self.builder.ret_void() else: @@ -456,11 +456,11 @@ class Visitor: self.builder.branch(finally_block) else: eid = ll.Constant(ll.IntType(32), node.exc.args[0].n) - self.env.build_raise(self.builder, eid) + self.runtime.build_raise(self.builder, eid) def _handle_exception(self, function, finally_block, propagate, propagate_eid, handlers): - eid = self.env.build_getid(self.builder) + eid = self.runtime.build_getid(self.builder) self._active_exception_stack.append( (finally_block, propagate, propagate_eid)) self.builder.store(ll.Constant(ll.IntType(1), 1), propagate) @@ -509,7 +509,7 @@ class Visitor: self.builder.store(ll.Constant(ll.IntType(1), 0), propagate) propagate_eid = self.builder.alloca(ll.IntType(32), name="propagate_eid") - exception_occured = self.env.build_catch(self.builder) + exception_occured = self.runtime.build_catch(self.builder) self.builder.cbranch(exception_occured, exc_block, noexc_block) self.builder.position_at_end(noexc_block) @@ -517,7 +517,7 @@ class Visitor: self.visit_statements(node.body) self._exception_level_stack[-1] -= 1 if not self._bb_terminated(): - self.env.build_pop(self.builder, 1) + self.runtime.build_pop(self.builder, 1) self.visit_statements(node.orelse) if not self._bb_terminated(): self.builder.branch(finally_block) @@ -534,6 +534,6 @@ class Visitor: self.builder.load(propagate), propagate_block, merge_block) self.builder.position_at_end(propagate_block) - self.env.build_raise(self.builder, self.builder.load(propagate_eid)) + self.runtime.build_raise(self.builder, self.builder.load(propagate_eid)) self.builder.branch(merge_block) self.builder.position_at_end(merge_block) diff --git a/artiq/py2llvm_old/base_types.py b/artiq/py2llvm_old/base_types.py index 9e6f809ef..3ef472984 100644 --- a/artiq/py2llvm_old/base_types.py +++ b/artiq/py2llvm_old/base_types.py @@ -1,4 +1,4 @@ -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll from artiq.py2llvm.values import VGeneric diff --git a/artiq/py2llvm_old/fractions.py b/artiq/py2llvm_old/fractions.py index 2b3fa6fd4..2aab95336 100644 --- a/artiq/py2llvm_old/fractions.py +++ b/artiq/py2llvm_old/fractions.py @@ -1,7 +1,7 @@ import inspect from pythonparser import parse, ast -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll from artiq.py2llvm.values import VGeneric, operators from artiq.py2llvm.base_types import VBool, VInt, VFloat diff --git a/artiq/py2llvm_old/lists.py b/artiq/py2llvm_old/lists.py index ded97773e..e17ab5348 100644 --- a/artiq/py2llvm_old/lists.py +++ b/artiq/py2llvm_old/lists.py @@ -1,4 +1,4 @@ -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll from artiq.py2llvm.values import VGeneric from artiq.py2llvm.base_types import VInt, VNone diff --git a/artiq/py2llvm_old/module.py b/artiq/py2llvm_old/module.py index 535b1ab51..f4df806e6 100644 --- a/artiq/py2llvm_old/module.py +++ b/artiq/py2llvm_old/module.py @@ -1,16 +1,16 @@ -import llvmlite.ir as ll -import llvmlite.binding as llvm +import llvmlite_or1k.ir as ll +import llvmlite_or1k.binding as llvm from artiq.py2llvm import infer_types, ast_body, base_types, fractions, tools class Module: - def __init__(self, env=None): + def __init__(self, runtime=None): self.llvm_module = ll.Module("main") - self.env = env + self.runtime = runtime - if self.env is not None: - self.env.init_module(self) + if self.runtime is not None: + self.runtime.init_module(self) fractions.init_module(self) def finalize(self): @@ -30,10 +30,10 @@ class Module: def emit_object(self): self.finalize() - return self.env.emit_object() + return self.runtime.emit_object() def compile_function(self, func_def, param_types): - ns = infer_types.infer_function_types(self.env, func_def, param_types) + ns = infer_types.infer_function_types(self.runtime, func_def, param_types) retval = ns["return"] function_type = ll.FunctionType(retval.get_llvm_type(), @@ -50,7 +50,7 @@ class Module: for arg_ast, arg_llvm in zip(func_def.args.args, function.args): ns[arg_ast.arg].auto_store(builder, arg_llvm) - visitor = ast_body.Visitor(self.env, ns, builder) + visitor = ast_body.Visitor(self.runtime, ns, builder) visitor.visit_statements(func_def.body) if not tools.is_terminated(builder.basic_block): diff --git a/artiq/py2llvm_old/tools.py b/artiq/py2llvm_old/tools.py index 27ffd8566..ba9e76949 100644 --- a/artiq/py2llvm_old/tools.py +++ b/artiq/py2llvm_old/tools.py @@ -1,4 +1,4 @@ -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll def is_terminated(basic_block): return (basic_block.instructions diff --git a/artiq/py2llvm_old/values.py b/artiq/py2llvm_old/values.py index 554aad7c6..6f0b90e2c 100644 --- a/artiq/py2llvm_old/values.py +++ b/artiq/py2llvm_old/values.py @@ -1,7 +1,7 @@ from types import SimpleNamespace from copy import copy -import llvmlite.ir as ll +import llvmlite_or1k.ir as ll class VGeneric: diff --git a/artiq/sim/devices.py b/artiq/sim/devices.py index c5900a405..3f11c1c1e 100644 --- a/artiq/sim/devices.py +++ b/artiq/sim/devices.py @@ -1,29 +1,29 @@ from random import Random from artiq.language.core import delay, kernel -from artiq.language.db import * from artiq.language import units from artiq.sim import time -class Core(AutoDB): - _level = 0 +class Core: + def __init__(self, dmgr): + self.ref_period = 1 + self._level = 0 def run(self, k_function, k_args, k_kwargs): - Core._level += 1 + self._level += 1 r = k_function(*k_args, **k_kwargs) - Core._level -= 1 - if Core._level == 0: + self._level -= 1 + if self._level == 0: print(time.manager.format_timeline()) return r -class Input(AutoDB): - class DBKeys: - core = Device() - name = Argument() +class Input: + def __init__(self, dmgr, name): + self.core = dmgr.get("core") + self.name = name - def build(self): self.prng = Random() @kernel @@ -40,10 +40,10 @@ class Input(AutoDB): return result -class WaveOutput(AutoDB): - class DBKeys: - core = Device() - name = Argument() +class WaveOutput: + def __init__(self, dmgr, name): + self.core = dmgr.get("core") + self.name = name @kernel def pulse(self, frequency, duration): @@ -51,10 +51,10 @@ class WaveOutput(AutoDB): delay(duration) -class VoltageOutput(AutoDB): - class DBKeys: - core = Device() - name = Argument() +class VoltageOutput: + def __init__(self, dmgr, name): + self.core = dmgr.get("core") + self.name = name @kernel def set(self, value): diff --git a/artiq/sim/time.py b/artiq/sim/time.py index 3ae01a073..72cd3313f 100644 --- a/artiq/sim/time.py +++ b/artiq/sim/time.py @@ -30,37 +30,39 @@ class Manager: self.timeline = [] def enter_sequential(self): - new_context = SequentialTimeContext(self.get_time()) + new_context = SequentialTimeContext(self.get_time_mu()) self.stack.append(new_context) def enter_parallel(self): - new_context = ParallelTimeContext(self.get_time()) + new_context = ParallelTimeContext(self.get_time_mu()) self.stack.append(new_context) def exit(self): old_context = self.stack.pop() self.take_time(old_context.block_duration) - def take_time(self, duration): + def take_time_mu(self, duration): self.stack[-1].take_time(duration) - def get_time(self): + def get_time_mu(self): return self.stack[-1].current_time - def set_time(self, t): - dt = t - self.get_time() + def set_time_mu(self, t): + dt = t - self.get_time_mu() if dt < 0*s: raise ValueError("Attempted to go back in time") self.take_time(dt) + take_time = take_time_mu + def event(self, description): - self.timeline.append((self.get_time(), description)) + self.timeline.append((self.get_time_mu(), description)) def format_timeline(self): r = "" prev_time = 0*s for time, description in sorted(self.timeline, key=itemgetter(0)): - r += "@{:10} (+{:10}) ".format(str(time), str(time-prev_time)) + r += "@{:.9f} (+{:.9f}) ".format(time, time-prev_time) for item in description: r += "{:16}".format(str(item)) r += "\n" diff --git a/artiq/test/coefficients.py b/artiq/test/coefficients.py index 20fd9ba99..35bb3f35f 100644 --- a/artiq/test/coefficients.py +++ b/artiq/test/coefficients.py @@ -12,7 +12,7 @@ class TestSplineCoef(unittest.TestCase): self.s = coefficients.SplineSource(self.x, self.y, order=4) def test_get_segment(self): - return list(self.s.get_segment_data(1.5, 3.2, 1/100.)) + return list(self.s.get_segment_data(start=1.5, stop=3.2, scale=.01)) def test_synth(self): d = self.test_get_segment() @@ -32,7 +32,7 @@ class TestSplineCoef(unittest.TestCase): @unittest.skip("manual/visual test") def test_plot(self): - import cairoplot + import matplotlib.pyplot as plt y = self.test_run() - x = list(range(len(y))) - cairoplot.scatter_plot("plot.png", [x, y]) + plt.step(np.arange(len(y)), y) + plt.show() diff --git a/artiq/test/coredevice.py b/artiq/test/coredevice.py new file mode 100644 index 000000000..95e8573b9 --- /dev/null +++ b/artiq/test/coredevice.py @@ -0,0 +1,276 @@ +from math import sqrt + +from artiq.language import * +from artiq.test.hardware_testbench import ExperimentCase +from artiq.coredevice.runtime_exceptions import RTIOUnderflow +from artiq.coredevice import runtime_exceptions + + +class RTT(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl_inout") + + def set_rtt(self, rtt): + self.set_result("rtt", rtt) + + @kernel + def run(self): + self.ttl_inout.output() + delay(1*us) + with parallel: + # make sure not to send two commands into the same RTIO + # channel with the same timestamp + self.ttl_inout.gate_rising(5*us) + with sequential: + delay(1*us) + t0 = now_mu() + self.ttl_inout.pulse(1*us) + self.set_rtt(mu_to_seconds(self.ttl_inout.timestamp_mu() - t0)) + + +class Loopback(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("loop_in") + self.attr_device("loop_out") + + def set_rtt(self, rtt): + self.set_result("rtt", rtt) + + @kernel + def run(self): + self.loop_in.input() + delay(1*us) + with parallel: + self.loop_in.gate_rising(2*us) + with sequential: + delay(1*us) + t0 = now_mu() + self.loop_out.pulse(1*us) + self.set_rtt(mu_to_seconds(self.loop_in.timestamp_mu() - t0)) + + +class ClockGeneratorLoopback(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("loop_clock_in") + self.attr_device("loop_clock_out") + + def set_count(self, count): + self.set_result("count", count) + + @kernel + def run(self): + self.loop_clock_in.input() + self.loop_clock_out.stop() + delay(1*us) + with parallel: + self.loop_clock_in.gate_rising(10*us) + with sequential: + delay(200*ns) + self.loop_clock_out.set(1*MHz) + self.set_count(self.loop_clock_in.count()) + + +class PulseRate(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("loop_out") + + def set_pulse_rate(self, pulse_rate): + self.set_result("pulse_rate", pulse_rate) + + @kernel + def run(self): + dt = seconds_to_mu(1000*ns) + while True: + try: + for i in range(1000): + self.loop_out.pulse_mu(dt) + delay_mu(dt) + except RTIOUnderflow: + dt += 1 + self.core.break_realtime() + else: + self.set_pulse_rate(mu_to_seconds(2*dt)) + break + + +class Watchdog(EnvExperiment): + def build(self): + self.attr_device("core") + + @kernel + def run(self): + with watchdog(50*ms): + while True: + pass + + +class LoopbackCount(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl_inout") + self.attr_argument("npulses") + + def set_count(self, count): + self.set_result("count", count) + + @kernel + def run(self): + self.ttl_inout.output() + delay(1*us) + with parallel: + self.ttl_inout.gate_rising(10*us) + with sequential: + for i in range(self.npulses): + delay(25*ns) + self.ttl_inout.pulse(25*ns) + self.set_count(self.ttl_inout.count()) + + +class Underflow(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl_out") + + @kernel + def run(self): + while True: + delay(25*ns) + self.ttl_out.pulse(25*ns) + + +class SequenceError(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl_out") + + @kernel + def run(self): + t = now_mu() + self.ttl_out.pulse(25*us) + at_mu(t) + self.ttl_out.pulse(25*us) + + +class TimeKeepsRunning(EnvExperiment): + def build(self): + self.attr_device("core") + + def set_time_at_start(self, time_at_start): + self.set_result("time_at_start", time_at_start) + + @kernel + def run(self): + self.set_time_at_start(now_mu()) + + +class Handover(EnvExperiment): + def build(self): + self.attr_device("core") + + @kernel + def get_now(self): + self.time_at_start = now_mu() + + def run(self): + self.get_now() + self.set_result("t1", self.time_at_start) + self.get_now() + self.set_result("t2", self.time_at_start) + + +class CoredeviceTest(ExperimentCase): + def test_rtt(self): + self.execute(RTT) + rtt = self.rdb.get("rtt") + print(rtt) + self.assertGreater(rtt, 0*ns) + self.assertLess(rtt, 100*ns) + + def test_loopback(self): + self.execute(Loopback) + rtt = self.rdb.get("rtt") + print(rtt) + self.assertGreater(rtt, 0*ns) + self.assertLess(rtt, 50*ns) + + def test_clock_generator_loopback(self): + self.execute(ClockGeneratorLoopback) + count = self.rdb.get("count") + self.assertEqual(count, 10) + + def test_pulse_rate(self): + self.execute(PulseRate) + rate = self.rdb.get("pulse_rate") + print(rate) + self.assertGreater(rate, 100*ns) + self.assertLess(rate, 2500*ns) + + def test_loopback_count(self): + npulses = 2 + r = self.execute(LoopbackCount, npulses=npulses) + count = self.rdb.get("count") + self.assertEqual(count, npulses) + + def test_underflow(self): + with self.assertRaises(runtime_exceptions.RTIOUnderflow): + self.execute(Underflow) + + def test_sequence_error(self): + with self.assertRaises(runtime_exceptions.RTIOSequenceError): + self.execute(SequenceError) + + def test_watchdog(self): + # watchdog only works on the device + with self.assertRaises(IOError): + self.execute(Watchdog) + + def test_time_keeps_running(self): + self.execute(TimeKeepsRunning) + t1 = self.rdb.get("time_at_start") + self.execute(TimeKeepsRunning) + t2 = self.rdb.get("time_at_start") + dead_time = mu_to_seconds(t2 - t1, self.dmgr.get("core")) + print(dead_time) + self.assertGreater(dead_time, 1*ms) + self.assertLess(dead_time, 300*ms) + + def test_handover(self): + self.execute(Handover) + self.assertEqual(self.rdb.get("t1"), self.rdb.get("t2")) + + +class RPCTiming(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("repeats", FreeValue(100)) + + def nop(self, x): + pass + + @kernel + def bench(self): + self.ts = [0. for _ in range(self.repeats)] + for i in range(self.repeats): + t1 = self.core.get_rtio_counter_mu() + self.nop(1) + t2 = self.core.get_rtio_counter_mu() + self.ts[i] = mu_to_seconds(t2 - t1) + + def run(self): + self.bench() + mean = sum(self.ts)/self.repeats + self.set_result("rpc_time_stddev", sqrt( + sum([(t - mean)**2 for t in self.ts])/self.repeats)) + self.set_result("rpc_time_mean", mean) + + +class RPCTest(ExperimentCase): + def test_rpc_timing(self): + self.execute(RPCTiming) + self.assertGreater(self.rdb.get("rpc_time_mean"), 100*ns) + self.assertLess(self.rdb.get("rpc_time_mean"), 15*ms) + self.assertLess(self.rdb.get("rpc_time_stddev"), 1*ms) diff --git a/artiq/test/coredevice_vs_host.py b/artiq/test/coredevice_vs_host.py new file mode 100644 index 000000000..31ef27da4 --- /dev/null +++ b/artiq/test/coredevice_vs_host.py @@ -0,0 +1,220 @@ +from operator import itemgetter +from fractions import Fraction + +from artiq import * +from artiq.sim import devices as sim_devices +from artiq.test.hardware_testbench import ExperimentCase + + +def _run_on_host(k_class, **arguments): + dmgr = dict() + dmgr["core"] = sim_devices.Core(dmgr) + k_inst = k_class(dmgr, **arguments) + k_inst.run() + return k_inst + + +class _Primes(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("output_list") + self.attr_argument("maximum") + + @kernel + def run(self): + for x in range(1, self.maximum): + d = 2 + prime = True + while d*d <= x: + if x % d == 0: + prime = False + break + d += 1 + if prime: + self.output_list.append(x) + + +class _Misc(EnvExperiment): + def build(self): + self.attr_device("core") + + self.input = 84 + self.al = [1, 2, 3, 4, 5] + self.list_copy_in = [2*Hz, 10*MHz] + + @kernel + def run(self): + self.half_input = self.input//2 + self.decimal_fraction = Fraction("1.2") + self.acc = 0 + for i in range(len(self.al)): + self.acc += self.al[i] + self.list_copy_out = self.list_copy_in + + +class _PulseLogger(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("output_list") + self.attr_argument("name") + + def _append(self, t, l, f): + if not hasattr(self, "first_timestamp"): + self.first_timestamp = t + self.output_list.append((self.name, t-self.first_timestamp, l, f)) + + def int_usec(self, mu): + return round(mu_to_seconds(mu, self.core)*1000000) + + def on(self, t, f): + self._append(self.int_usec(t), True, f) + + def off(self, t): + self._append(self.int_usec(t), False, 0) + + @kernel + def pulse(self, f, duration): + self.on(now_mu(), f) + delay(duration) + self.off(now_mu()) + + +class _Pulses(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("output_list") + + for name in "a", "b", "c", "d": + pl = _PulseLogger(*self.dbs(), + output_list=self.output_list, + name=name) + setattr(self, name, pl) + + @kernel + def run(self): + for i in range(3): + with parallel: + with sequential: + self.a.pulse(100+i, 20*us) + self.b.pulse(200+i, 20*us) + with sequential: + self.c.pulse(300+i, 10*us) + self.d.pulse(400+i, 20*us) + + +class _MyException(Exception): + pass + + +class _Exceptions(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("trace") + + @kernel + def run(self): + for i in range(10): + self.trace.append(i) + if i == 4: + try: + self.trace.append(10) + try: + self.trace.append(11) + break + except: + pass + else: + self.trace.append(12) + try: + self.trace.append(13) + except: + pass + except _MyException: + self.trace.append(14) + + for i in range(4): + try: + self.trace.append(100) + if i == 1: + raise _MyException + elif i == 2: + raise IndexError + except (TypeError, IndexError): + self.trace.append(101) + raise + except: + self.trace.append(102) + else: + self.trace.append(103) + finally: + self.trace.append(104) + + +class _RPCExceptions(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_argument("catch", FreeValue(False)) + + self.success = False + + def exception_raiser(self): + raise _MyException + + @kernel + def run(self): + if self.catch: + self.do_catch() + else: + self.do_not_catch() + + @kernel + def do_not_catch(self): + self.exception_raiser() + + @kernel + def do_catch(self): + try: + self.exception_raiser() + except _MyException: + self.success = True + + +class HostVsDeviceCase(ExperimentCase): + def test_primes(self): + l_device, l_host = [], [] + self.execute(_Primes, maximum=100, output_list=l_device) + _run_on_host(_Primes, maximum=100, output_list=l_host) + self.assertEqual(l_device, l_host) + + def test_misc(self): + for f in self.execute, _run_on_host: + uut = f(_Misc) + self.assertEqual(uut.half_input, 42) + self.assertEqual(uut.decimal_fraction, Fraction("1.2")) + self.assertEqual(uut.acc, sum(uut.al)) + self.assertEqual(uut.list_copy_in, uut.list_copy_out) + + def test_pulses(self): + l_device, l_host = [], [] + self.execute(_Pulses, output_list=l_device) + _run_on_host(_Pulses, output_list=l_host) + l_host = sorted(l_host, key=itemgetter(1)) + for channel in "a", "b", "c", "d": + c_device = [x for x in l_device if x[0] == channel] + c_host = [x for x in l_host if x[0] == channel] + self.assertEqual(c_device, c_host) + + def test_exceptions(self): + t_device, t_host = [], [] + with self.assertRaises(IndexError): + self.execute(_Exceptions, trace=t_device) + with self.assertRaises(IndexError): + _run_on_host(_Exceptions, trace=t_host) + self.assertEqual(t_device, t_host) + + def test_rpc_exceptions(self): + for f in self.execute, _run_on_host: + with self.assertRaises(_MyException): + f(_RPCExceptions, catch=False) + uut = self.execute(_RPCExceptions, catch=True) + self.assertTrue(uut.success) diff --git a/artiq/test/full_stack.py b/artiq/test/full_stack.py deleted file mode 100644 index 83b287483..000000000 --- a/artiq/test/full_stack.py +++ /dev/null @@ -1,371 +0,0 @@ -import unittest -from operator import itemgetter -import os -from fractions import Fraction - -from artiq import * -from artiq.language.units import DimensionError -from artiq.coredevice import comm_tcp, core, runtime_exceptions, ttl -from artiq.sim import devices as sim_devices - - -core_device = os.getenv("ARTIQ_CORE_DEVICE") - - -def _run_on_device(k_class, **parameters): - comm = comm_tcp.Comm(host=core_device) - try: - coredev = core.Core(comm=comm) - k_inst = k_class(core=coredev, **parameters) - k_inst.run() - finally: - comm.close() - - -def _run_on_host(k_class, **parameters): - coredev = sim_devices.Core() - k_inst = k_class(core=coredev, **parameters) - k_inst.run() - - -class _Primes(AutoDB): - class DBKeys: - core = Device() - output_list = Argument() - maximum = Argument() - - @kernel - def run(self): - for x in range(1, self.maximum): - d = 2 - prime = True - while d*d <= x: - if x % d == 0: - prime = False - break - d += 1 - if prime: - self.output_list.append(x) - - -class _Misc(AutoDB): - def build(self): - self.input = 84 - self.inhomogeneous_units = [] - self.al = [1, 2, 3, 4, 5] - self.list_copy_in = [2*Hz, 10*MHz] - - @kernel - def run(self): - self.half_input = self.input//2 - self.decimal_fraction = Fraction("1.2") - self.inhomogeneous_units.append(1000*Hz) - self.inhomogeneous_units.append(10*s) - self.acc = 0 - for i in range(len(self.al)): - self.acc += self.al[i] - self.list_copy_out = self.list_copy_in - self.unit_comp = [1*MHz for _ in range(3)] - - @kernel - def dimension_error1(self): - print(1*Hz + 1*s) - - @kernel - def dimension_error2(self): - print(1*Hz < 1*s) - - @kernel - def dimension_error3(self): - check_unit(1*Hz, "s") - - @kernel - def dimension_error4(self): - delay(10*Hz) - - -class _PulseLogger(AutoDB): - class DBKeys: - core = Device() - output_list = Argument() - name = Argument() - - def _append(self, t, l, f): - if not hasattr(self, "first_timestamp"): - self.first_timestamp = t - self.output_list.append((self.name, t-self.first_timestamp, l, f)) - - def on(self, t, f): - self._append(t, True, f) - - def off(self, t): - self._append(t, False, 0) - - @kernel - def pulse(self, f, duration): - self.on(int(now().amount*1000000000), f) - delay(duration) - self.off(int(now().amount*1000000000)) - - -class _Pulses(AutoDB): - class DBKeys: - core = Device() - output_list = Argument() - - def build(self): - for name in "a", "b", "c", "d": - pl = _PulseLogger(core=self.core, - output_list=self.output_list, - name=name) - setattr(self, name, pl) - - @kernel - def run(self): - for i in range(3): - with parallel: - with sequential: - self.a.pulse(100+i, 20*us) - self.b.pulse(200+i, 20*us) - with sequential: - self.c.pulse(300+i, 10*us) - self.d.pulse(400+i, 20*us) - - -class _MyException(Exception): - pass - - -class _Exceptions(AutoDB): - class DBKeys: - core = Device() - trace = Argument() - - @kernel - def run(self): - for i in range(10): - self.trace.append(i) - if i == 4: - try: - self.trace.append(10) - try: - self.trace.append(11) - break - except: - pass - else: - self.trace.append(12) - try: - self.trace.append(13) - except: - pass - except _MyException: - self.trace.append(14) - - for i in range(4): - try: - self.trace.append(100) - if i == 1: - raise _MyException - elif i == 2: - raise IndexError - except (TypeError, IndexError): - self.trace.append(101) - raise - except: - self.trace.append(102) - else: - self.trace.append(103) - finally: - self.trace.append(104) - - -class _RPCExceptions(AutoDB): - class DBKeys: - core = Device() - - def build(self): - self.success = False - - def exception_raiser(self): - raise _MyException - - @kernel - def do_not_catch(self): - self.exception_raiser() - - @kernel - def catch(self): - try: - self.exception_raiser() - except _MyException: - self.success = True - - -class _Watchdog(AutoDB): - class DBKeys: - core = Device() - - @kernel - def run(self): - with watchdog(50*ms): - while True: - pass - - -@unittest.skipUnless(core_device, "no hardware") -class ExecutionCase(unittest.TestCase): - def test_primes(self): - l_device, l_host = [], [] - _run_on_device(_Primes, maximum=100, output_list=l_device) - _run_on_host(_Primes, maximum=100, output_list=l_host) - self.assertEqual(l_device, l_host) - - def test_misc(self): - comm = comm_tcp.Comm(host=core_device) - try: - coredev = core.Core(comm=comm) - uut = _Misc(core=coredev) - uut.run() - self.assertEqual(uut.half_input, 42) - self.assertEqual(uut.decimal_fraction, Fraction("1.2")) - self.assertEqual(uut.inhomogeneous_units, [1000*Hz, 10*s]) - self.assertEqual(uut.acc, sum(uut.al)) - self.assertEqual(uut.list_copy_in, uut.list_copy_out) - self.assertEqual(uut.unit_comp, [1*MHz for _ in range(3)]) - with self.assertRaises(DimensionError): - uut.dimension_error1() - with self.assertRaises(DimensionError): - uut.dimension_error2() - with self.assertRaises(DimensionError): - uut.dimension_error3() - with self.assertRaises(DimensionError): - uut.dimension_error4() - finally: - comm.close() - - def test_pulses(self): - l_device, l_host = [], [] - _run_on_device(_Pulses, output_list=l_device) - _run_on_host(_Pulses, output_list=l_host) - l_host = sorted(l_host, key=itemgetter(1)) - for channel in "a", "b", "c", "d": - c_device = [x for x in l_device if x[0] == channel] - c_host = [x for x in l_host if x[0] == channel] - self.assertEqual(c_device, c_host) - - def test_exceptions(self): - t_device, t_host = [], [] - with self.assertRaises(IndexError): - _run_on_device(_Exceptions, trace=t_device) - with self.assertRaises(IndexError): - _run_on_host(_Exceptions, trace=t_host) - self.assertEqual(t_device, t_host) - - def test_rpc_exceptions(self): - comm = comm_tcp.Comm(host=core_device) - try: - uut = _RPCExceptions(core=core.Core(comm=comm)) - with self.assertRaises(_MyException): - uut.do_not_catch() - uut.catch() - self.assertTrue(uut.success) - finally: - comm.close() - - def test_watchdog(self): - with self.assertRaises(IOError): - _run_on_device(_Watchdog) - - -class _RTIOLoopback(AutoDB): - class DBKeys: - core = Device() - io = Device() - npulses = Argument() - - def report(self, n): - self.result = n - - @kernel - def run(self): - self.io.output() - delay(1*us) - with parallel: - self.io.gate_rising(10*us) - with sequential: - for i in range(self.npulses): - delay(25*ns) - self.io.pulse(25*ns) - self.report(self.io.count()) - - -class _RTIOUnderflow(AutoDB): - class DBKeys: - core = Device() - o = Device() - - @kernel - def run(self): - while True: - delay(25*ns) - self.o.pulse(25*ns) - - -class _RTIOSequenceError(AutoDB): - class DBKeys: - core = Device() - o = Device() - - @kernel - def run(self): - t = now() - self.o.pulse(25*us) - at(t) - self.o.pulse(25*us) - - -@unittest.skipUnless(core_device, "no hardware") -class RTIOCase(unittest.TestCase): - # Connect channels 0 and 1 together for this test - # (C11 and C13 on Papilio Pro) - def test_loopback(self): - npulses = 4 - comm = comm_tcp.Comm(host=core_device) - try: - coredev = core.Core(comm=comm) - uut = _RTIOLoopback( - core=coredev, - io=ttl.TTLInOut(core=coredev, channel=0), - npulses=npulses - ) - uut.run() - self.assertEqual(uut.result, npulses) - finally: - comm.close() - - def test_underflow(self): - comm = comm_tcp.Comm(host=core_device) - try: - coredev = core.Core(comm=comm) - uut = _RTIOUnderflow( - core=coredev, - o=ttl.TTLOut(core=coredev, channel=2) - ) - with self.assertRaises(runtime_exceptions.RTIOUnderflow): - uut.run() - finally: - comm.close() - - def test_sequence_error(self): - comm = comm_tcp.Comm(host=core_device) - try: - coredev = core.Core(comm=comm) - uut = _RTIOSequenceError( - core=coredev, - o=ttl.TTLOut(core=coredev, channel=2) - ) - with self.assertRaises(runtime_exceptions.RTIOSequenceError): - uut.run() - finally: - comm.close() diff --git a/artiq/test/hardware_testbench.py b/artiq/test/hardware_testbench.py new file mode 100644 index 000000000..1734852cc --- /dev/null +++ b/artiq/test/hardware_testbench.py @@ -0,0 +1,58 @@ +import os +import sys +import unittest +import logging + +from artiq.language import * +from artiq.protocols.file_db import FlatFileDB +from artiq.master.worker_db import DeviceManager, ResultDB +from artiq.frontend.artiq_run import DummyScheduler + + +artiq_root = os.getenv("ARTIQ_ROOT") +logger = logging.getLogger(__name__) + + +def get_from_ddb(*path, default="skip"): + if not artiq_root: + raise unittest.SkipTest("no ARTIQ_ROOT") + v = FlatFileDB(os.path.join(artiq_root, "ddb.pyon")).data + try: + for p in path: + v = v[p] + return v.read + except KeyError: + if default == "skip": + raise unittest.SkipTest("ddb path {} not found".format(path)) + else: + return default + + +@unittest.skipUnless(artiq_root, "no ARTIQ_ROOT") +class ExperimentCase(unittest.TestCase): + def setUp(self): + self.ddb = FlatFileDB(os.path.join(artiq_root, "ddb.pyon")) + self.dmgr = DeviceManager(self.ddb, + virtual_devices={"scheduler": DummyScheduler()}) + self.pdb = FlatFileDB(os.path.join(artiq_root, "pdb.pyon")) + self.rdb = ResultDB() + + def execute(self, cls, **kwargs): + expid = { + "file": sys.modules[cls.__module__].__file__, + "class_name": cls.__name__, + "arguments": kwargs + } + self.dmgr.virtual_devices["scheduler"].expid = expid + try: + try: + exp = cls(self.dmgr, self.pdb, self.rdb, **kwargs) + except KeyError as e: + # skip if ddb does not match requirements + raise unittest.SkipTest(*e.args) + exp.prepare() + exp.run() + exp.analyze() + return exp + finally: + self.dmgr.close_devices() diff --git a/artiq/test/lda.py b/artiq/test/lda.py index 9820bc226..5231b7964 100644 --- a/artiq/test/lda.py +++ b/artiq/test/lda.py @@ -1,34 +1,28 @@ import unittest -import os from artiq.devices.lda.driver import Lda, Ldasim from artiq.language.units import dB - - -lda_serial = os.getenv("ARTIQ_LDA_SERIAL") +from artiq.test.hardware_testbench import get_from_ddb class GenericLdaTest: def test_attenuation(self): - step = self.cont.get_att_step_size().amount - max = self.cont.get_att_max().amount - test_vector = [i*step*dB for i in range(0, int(max*int(1/step)+1))] + step = self.cont.get_att_step_size() + attmax = self.cont.get_att_max() + test_vector = [i*step*dB for i in range(0, int(attmax*int(1/step)+1))] for i in test_vector: with self.subTest(i=i): self.cont.set_attenuation(i) self.assertEqual(i, self.cont.get_attenuation()) -@unittest.skipUnless(lda_serial, "no hardware") class TestLda(GenericLdaTest, unittest.TestCase): def setUp(self): - product = os.getenv("ARTIQ_LDA_PRODUCT") - self.cont = Lda(serial=lda_serial, product=product) + lda_serial = get_from_ddb("lda", "device") + lda_product = get_from_ddb("lda", "product") + self.cont = Lda(serial=lda_serial, product=lda_product) class TestLdaSim(GenericLdaTest, unittest.TestCase): def setUp(self): self.cont = Ldasim() - -if __name__ == "__main__": - unittest.main() diff --git a/artiq/test/novatech409b.py b/artiq/test/novatech409b.py new file mode 100644 index 000000000..314b1cb19 --- /dev/null +++ b/artiq/test/novatech409b.py @@ -0,0 +1,31 @@ +import unittest + +from artiq.devices.novatech409b.driver import Novatech409B +from artiq.test.hardware_testbench import get_from_ddb + + +class GenericNovatech409BTest: + def test_parameters_readback(self): + # write sample data and read it back + for i in range(4): + self.driver.set_freq(i, 1e6) + self.driver.set_phase(i, 0.5) + self.driver.set_gain(i, 0.25) + result = self.driver.get_status() + + # check for expected status message; ignore all but first 23 bytes + # compare with previous result extracted from Novatech + for i in range(4): + r = result[i] + self.assertEqual(r[0:23], "00989680 2000 01F5 0000") + + +class TestNovatech409B(GenericNovatech409BTest, unittest.TestCase): + def setUp(self): + novatech409b_device = get_from_ddb("novatech409b", "device") + self.driver = Novatech409B(novatech409b_device) + + +class TestNovatech409BSim(GenericNovatech409BTest, unittest.TestCase): + def setUp(self): + self.driver = Novatech409B(None) diff --git a/artiq/test/pc_rpc.py b/artiq/test/pc_rpc.py index bbd4dde80..1b60d245f 100644 --- a/artiq/test/pc_rpc.py +++ b/artiq/test/pc_rpc.py @@ -6,7 +6,7 @@ import time import numpy as np -from artiq.protocols import pc_rpc +from artiq.protocols import pc_rpc, fire_and_forget test_address = "::1" @@ -73,13 +73,29 @@ class RPCCase(unittest.TestCase): remote.close_rpc() def _loop_asyncio_echo(self): - loop = asyncio.get_event_loop() - loop.run_until_complete(self._asyncio_echo()) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(self._asyncio_echo()) + finally: + loop.close() def test_asyncio_echo(self): self._run_server_and_test(self._loop_asyncio_echo) +class FireAndForgetCase(unittest.TestCase): + def _set_ok(self): + self.ok = True + + def test_fire_and_forget(self): + self.ok = False + p = fire_and_forget.FFProxy(self) + p._set_ok() + p.ff_join() + self.assertTrue(self.ok) + + class Echo: def __init__(self): self.terminate_notify = asyncio.Semaphore(0) @@ -96,7 +112,8 @@ class Echo: def run_server(): - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) try: echo = Echo() server = pc_rpc.Server({"test": echo}) diff --git a/artiq/test/py2llvm.py b/artiq/test/py2llvm.py index 7f01d983c..07250b7d1 100644 --- a/artiq/test/py2llvm.py +++ b/artiq/test/py2llvm.py @@ -5,7 +5,7 @@ from fractions import Fraction from ctypes import CFUNCTYPE, c_int, c_int32, c_int64, c_double import struct -import llvmlite.binding as llvm +import llvmlite_or1k.binding as llvm from artiq.language.core import int64 from artiq.py2llvm.infer_types import infer_function_types diff --git a/artiq/test/scheduler.py b/artiq/test/scheduler.py new file mode 100644 index 000000000..9c1b2717e --- /dev/null +++ b/artiq/test/scheduler.py @@ -0,0 +1,169 @@ +import unittest +import asyncio +import sys +from time import time, sleep + +from artiq import * +from artiq.master.scheduler import Scheduler + + +class EmptyExperiment(EnvExperiment): + def build(self): + pass + + def run(self): + pass + + +class BackgroundExperiment(EnvExperiment): + def build(self): + self.attr_device("scheduler") + + def run(self): + while True: + self.scheduler.pause() + sleep(0.2) + + +def _get_expid(name): + return { + "file": sys.modules[__name__].__file__, + "class_name": name, + "arguments": dict() + } + + +def _get_basic_steps(rid, expid, priority=0, flush=False): + return [ + {"action": "setitem", "key": rid, "value": + {"pipeline": "main", "status": "pending", "priority": priority, + "expid": expid, "due_date": None, "flush": flush}, + "path": []}, + {"action": "setitem", "key": "status", "value": "preparing", + "path": [rid]}, + {"action": "setitem", "key": "status", "value": "prepare_done", + "path": [rid]}, + {"action": "setitem", "key": "status", "value": "running", + "path": [rid]}, + {"action": "setitem", "key": "status", "value": "run_done", + "path": [rid]}, + {"action": "setitem", "key": "status", "value": "analyzing", + "path": [rid]}, + {"action": "setitem", "key": "status", "value": "analyze_done", + "path": [rid]}, + {"action": "delitem", "key": rid, "path": []} + ] + + +_handlers = { + "init_rt_results": lambda description: None +} + + +class SchedulerCase(unittest.TestCase): + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def test_steps(self): + loop = self.loop + scheduler = Scheduler(0, _handlers) + expid = _get_expid("EmptyExperiment") + + expect = _get_basic_steps(1, expid) + done = asyncio.Event() + expect_idx = 0 + def notify(mod): + nonlocal expect_idx + self.assertEqual(mod, expect[expect_idx]) + expect_idx += 1 + if expect_idx >= len(expect): + done.set() + scheduler.notifier.publish = notify + + scheduler.start() + + # Verify that a timed experiment far in the future does not + # get run, even if it has high priority. + late = time() + 100000 + expect.insert(0, + {"action": "setitem", "key": 0, "value": + {"pipeline": "main", "status": "pending", "priority": 99, + "expid": expid, "due_date": late, "flush": False}, + "path": []}) + scheduler.submit("main", expid, 99, late, False) + + # This one (RID 1) gets run instead. + scheduler.submit("main", expid, 0, None, False) + + loop.run_until_complete(done.wait()) + scheduler.notifier.publish = None + loop.run_until_complete(scheduler.stop()) + + def test_pause(self): + loop = self.loop + scheduler = Scheduler(0, _handlers) + expid_bg = _get_expid("BackgroundExperiment") + expid = _get_expid("EmptyExperiment") + + expect = _get_basic_steps(1, expid) + background_running = asyncio.Event() + done = asyncio.Event() + expect_idx = 0 + def notify(mod): + nonlocal expect_idx + if mod == {"path": [0], + "value": "running", + "key": "status", + "action": "setitem"}: + background_running.set() + if mod["path"] == [1] or (mod["path"] == [] and mod["key"] == 1): + self.assertEqual(mod, expect[expect_idx]) + expect_idx += 1 + if expect_idx >= len(expect): + done.set() + scheduler.notifier.publish = notify + + scheduler.start() + scheduler.submit("main", expid_bg, -99, None, False) + loop.run_until_complete(background_running.wait()) + scheduler.submit("main", expid, 0, None, False) + loop.run_until_complete(done.wait()) + loop.run_until_complete(scheduler.stop()) + + def test_flush(self): + loop = self.loop + scheduler = Scheduler(0, _handlers) + expid = _get_expid("EmptyExperiment") + + expect = _get_basic_steps(1, expid, 1, True) + expect.insert(1, {"key": "status", + "path": [1], + "value": "flushing", + "action": "setitem"}) + first_preparing = asyncio.Event() + done = asyncio.Event() + expect_idx = 0 + def notify(mod): + nonlocal expect_idx + if mod == {"path": [0], + "value": "preparing", + "key": "status", + "action": "setitem"}: + first_preparing.set() + if mod["path"] == [1] or (mod["path"] == [] and mod["key"] == 1): + self.assertEqual(mod, expect[expect_idx]) + expect_idx += 1 + if expect_idx >= len(expect): + done.set() + scheduler.notifier.publish = notify + + scheduler.start() + scheduler.submit("main", expid, 0, None, False) + loop.run_until_complete(first_preparing.wait()) + scheduler.submit("main", expid, 1, None, True) + loop.run_until_complete(done.wait()) + loop.run_until_complete(scheduler.stop()) + + def tearDown(self): + self.loop.close() diff --git a/artiq/test/sync_struct.py b/artiq/test/sync_struct.py new file mode 100644 index 000000000..d8b229d7e --- /dev/null +++ b/artiq/test/sync_struct.py @@ -0,0 +1,82 @@ +import unittest +import asyncio +import numpy as np + +from artiq.protocols import sync_struct + +test_address = "::1" +test_port = 7777 + + +@asyncio.coroutine +def write_test_data(test_dict): + test_values = [5, 2.1, None, True, False, + {"a": 5, 2: np.linspace(0, 10, 1)}, + (4, 5), (10,), "ab\nx\"'"] + for i in range(10): + test_dict[str(i)] = i + for key, value in enumerate(test_values): + test_dict[key] = value + test_dict[1.5] = 1.5 + test_dict["array"] = [] + test_dict["array"].append(42) + test_dict["array"].insert(1, 1) + test_dict[100] = 0 + test_dict[100] = 1 + test_dict[101] = 1 + test_dict.pop(101) + test_dict[102] = 1 + del test_dict[102] + test_dict["finished"] = True + + +@asyncio.coroutine +def start_server(publisher_future, test_dict_future): + test_dict = sync_struct.Notifier(dict()) + publisher = sync_struct.Publisher( + {"test": test_dict}) + yield from publisher.start(test_address, test_port) + publisher_future.set_result(publisher) + test_dict_future.set_result(test_dict) + + +class SyncStructCase(unittest.TestCase): + def init_test_dict(self, init): + self.test_dict = init + return init + + def notify(self, mod): + if ((mod["action"] == "init" and "finished" in mod["struct"]) + or (mod["action"] == "setitem" and mod["key"] == "finished")): + self.receiving_done.set() + + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + def test_recv(self): + loop = self.loop + self.receiving_done = asyncio.Event() + publisher = asyncio.Future() + test_dict = asyncio.Future() + asyncio.async(start_server(publisher, test_dict)) + loop.run_until_complete(publisher) + loop.run_until_complete(test_dict) + + self.publisher = publisher.result() + test_dict = test_dict.result() + test_vector = dict() + loop.run_until_complete(write_test_data(test_vector)) + + asyncio.async(write_test_data(test_dict)) + self.subscriber = sync_struct.Subscriber("test", self.init_test_dict, + self.notify) + loop.run_until_complete(self.subscriber.connect(test_address, + test_port)) + loop.run_until_complete(self.receiving_done.wait()) + self.assertEqual(self.test_dict, test_vector) + self.loop.run_until_complete(self.subscriber.close()) + self.loop.run_until_complete(self.publisher.stop()) + + def tearDown(self): + self.loop.close() diff --git a/artiq/test/thorlabs_tcube.py b/artiq/test/thorlabs_tcube.py index c5b0ed21d..0ecb362e8 100644 --- a/artiq/test/thorlabs_tcube.py +++ b/artiq/test/thorlabs_tcube.py @@ -1,9 +1,9 @@ import unittest -import os import time from artiq.devices.thorlabs_tcube.driver import Tdc, Tpz, TdcSim, TpzSim from artiq.language.units import V +from artiq.test.hardware_testbench import get_from_ddb class GenericTdcTest: @@ -88,7 +88,7 @@ class GenericTpzTest: def test_ouput_volts(self): for voltage in 5*V, 10*V, 15*V, \ - round(self.cont.get_tpz_io_settings()[0].amount)*V: + round(self.cont.get_tpz_io_settings()[0])*V: with self.subTest(voltage=voltage): test_vector = voltage self.cont.set_output_volts(test_vector) @@ -131,12 +131,9 @@ class GenericTpzTest: self.assertEqual(test_vector, self.cont.get_tpz_io_settings()) -tdc_serial = os.getenv("ARTIQ_TDC_SERIAL") - - -@unittest.skipUnless(tdc_serial, "no hardware") class TestTdc(unittest.TestCase, GenericTdcTest): def setUp(self): + tdc_serial = get_from_ddb("tdc", "device") self.cont = Tdc(serial_dev=tdc_serial) @@ -145,12 +142,9 @@ class TestTdcSim(unittest.TestCase, GenericTdcTest): self.cont = TdcSim() -tpz_serial = os.getenv("ARTIQ_TPZ_SERIAL") - - -@unittest.skipUnless(tpz_serial, "no hardware") class TestTpz(unittest.TestCase, GenericTpzTest): def setUp(self): + tpz_serial = get_from_ddb("tpz", "device") self.cont = Tpz(serial_dev=tpz_serial) diff --git a/artiq/test/transforms.py b/artiq/test/transforms.py index e51995ad8..dffee41a2 100644 --- a/artiq/test/transforms.py +++ b/artiq/test/transforms.py @@ -6,28 +6,20 @@ from artiq.coredevice import comm_dummy, core from artiq.transforms.unparse import unparse -# Original code before inline: -# -# n = time_to_cycles(1.2345*ns) -# ftw = self.dds.frequency_to_ftw(345*MHz) -# f = self.dds.ftw_to_frequency(ftw) -# phi = 1000*cycles_to_time(n)*f -# do_someting(int(phi)) -# optimize_in = """ def run(): - dds_sysclk = Quantity(Fraction(1000000000, 1), 'Hz') - n = time_to_cycles((1.2345 * Quantity(Fraction(1, 1000000000), 's'))) + dds_sysclk = Fraction(1000000000, 1) + n = seconds_to_mu((1.2345 * Fraction(1, 1000000000))) with sequential: - frequency = (345 * Quantity(Fraction(1000000, 1), 'Hz')) + frequency = 345 * Fraction(1000000, 1) frequency_to_ftw_return = int((((2 ** 32) * frequency) / dds_sysclk)) ftw = frequency_to_ftw_return with sequential: ftw2 = ftw ftw_to_frequency_return = ((ftw2 * dds_sysclk) / (2 ** 32)) f = ftw_to_frequency_return - phi = ((1000 * cycles_to_time(n)) * f) + phi = ((1000 * mu_to_seconds(n)) * f) do_something(int(phi)) """ @@ -44,7 +36,9 @@ def run(): class OptimizeCase(unittest.TestCase): def test_optimize(self): - coredev = core.Core(comm=comm_dummy.Comm(), ref_period=1*ns) + dmgr = dict() + dmgr["comm"] = comm_dummy.Comm(dmgr) + coredev = core.Core(dmgr, ref_period=1*ns) func_def = ast.parse(optimize_in).body[0] coredev.transform_stack(func_def, dict(), dict()) self.assertEqual(unparse(func_def), optimize_out) diff --git a/artiq/test/worker.py b/artiq/test/worker.py index ecde74104..b40e7b6c8 100644 --- a/artiq/test/worker.py +++ b/artiq/test/worker.py @@ -7,20 +7,26 @@ from artiq import * from artiq.master.worker import * -class WatchdogNoTimeout(Experiment, AutoDB): +class WatchdogNoTimeout(EnvExperiment): + def build(self): + pass + def run(self): for i in range(10): with watchdog(0.5*s): sleep(0.1) -class WatchdogTimeout(Experiment, AutoDB): +class WatchdogTimeout(EnvExperiment): + def build(self): + pass + def run(self): with watchdog(0.1*s): sleep(100.0) -class WatchdogTimeoutInBuild(Experiment, AutoDB): +class WatchdogTimeoutInBuild(EnvExperiment): def build(self): with watchdog(0.1*s): sleep(100.0) @@ -31,32 +37,33 @@ class WatchdogTimeoutInBuild(Experiment, AutoDB): @asyncio.coroutine def _call_worker(worker, expid): - yield from worker.prepare(0, "main", expid, 0) try: + yield from worker.build(0, "main", expid, 0) + yield from worker.prepare() yield from worker.run() yield from worker.analyze() finally: yield from worker.close() -def _run_experiment(experiment): +def _run_experiment(class_name): expid = { "file": sys.modules[__name__].__file__, - "experiment": experiment, + "class_name": class_name, "arguments": dict() } - handlers = { - "init_rt_results": lambda description: None - } - - worker = Worker(handlers) loop = asyncio.get_event_loop() + worker = Worker() loop.run_until_complete(_call_worker(worker, expid)) class WatchdogCase(unittest.TestCase): + def setUp(self): + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + def test_watchdog_no_timeout(self): - _run_experiment("WatchdogNoTimeout") + _run_experiment("WatchdogNoTimeout") def test_watchdog_timeout(self): with self.assertRaises(WorkerWatchdogTimeout): @@ -65,3 +72,6 @@ class WatchdogCase(unittest.TestCase): def test_watchdog_timeout_in_build(self): with self.assertRaises(WorkerWatchdogTimeout): _run_experiment("WatchdogTimeoutInBuild") + + def tearDown(self): + self.loop.close() diff --git a/artiq/tools.py b/artiq/tools.py index 2b5400a93..767e858dd 100644 --- a/artiq/tools.py +++ b/artiq/tools.py @@ -7,7 +7,7 @@ import asyncio import time import os.path -from artiq.language.experiment import is_experiment +from artiq.language.environment import is_experiment from artiq.protocols import pyon @@ -18,15 +18,6 @@ def parse_arguments(arguments): d[name] = pyon.decode(value) return d -def format_arguments(arguments): - fmtargs = [] - for k, v in sorted(arguments.items(), key=itemgetter(0)): - fmtargs.append(k + "=" + repr(v)) - if fmtargs: - return ", ".join(fmtargs) - else: - return "" - def file_import(filename): linecache.checkcache(filename) @@ -90,12 +81,21 @@ def asyncio_process_wait_timeout(process, timeout): # causes a futures.InvalidStateError inside asyncio if and when the # process terminates after the timeout. # Work around this problem. - end_time = time.monotonic() + timeout + @asyncio.coroutine + def process_wait_returncode_timeout(): + while True: + if process.returncode is not None: + break + yield from asyncio.sleep(0.1) + yield from asyncio.wait_for(process_wait_returncode_timeout(), + timeout=timeout) + +@asyncio.coroutine +def asyncio_process_wait(process): r = True while r: - r = yield from asyncio.wait_for( - process.stdout.read(1024), - timeout=end_time - time.monotonic()) + f, p = yield from asyncio.wait([process.stdout.read(1024)]) + r = f.pop().result() @asyncio.coroutine @@ -119,3 +119,42 @@ def asyncio_queue_peek(q): return q._queue[0] else: raise asyncio.QueueEmpty + + +class TaskObject: + def start(self): + self.task = asyncio.async(self._do()) + + @asyncio.coroutine + def stop(self): + self.task.cancel() + yield from asyncio.wait([self.task]) + del self.task + + @asyncio.coroutine + def _do(self): + raise NotImplementedError + + +class WaitSet: + def __init__(self): + self._s = set() + self._ev = asyncio.Event() + + def _update_ev(self): + if self._s: + self._ev.clear() + else: + self._ev.set() + + def add(self, e): + self._s.add(e) + self._update_ev() + + def discard(self, e): + self._s.discard(e) + self._update_ev() + + @asyncio.coroutine + def wait_empty(self): + yield from self._ev.wait() diff --git a/artiq/transforms/interleave.py b/artiq/transforms/interleave.py index 0a1b794a9..8aa9df838 100644 --- a/artiq/transforms/interleave.py +++ b/artiq/transforms/interleave.py @@ -28,13 +28,14 @@ def _get_duration(stmt): return -1 elif isinstance(stmt, ast.Call): name = stmt.func.id - if name == "delay": + assert(name != "delay") + if name == "delay_mu": try: da = eval_constant(stmt.args[0]) except NotConstant: da = -1 return da - elif name == "at": + elif name == "at_mu": return -1 else: return 0 @@ -69,7 +70,7 @@ def _interleave_timelines(timelines): ref_stmt = stmt.stmt delay_stmt = ast.copy_location( ast.Expr(ast.Call( - func=ast.Name("delay", ast.Load()), + func=ast.Name("delay_mu", ast.Load()), args=[value_to_ast(dt)], keywords=[], starargs=[], kwargs=[])), ref_stmt) diff --git a/artiq/transforms/lower_time.py b/artiq/transforms/lower_time.py index c2c779b84..5b3df0245 100644 --- a/artiq/transforms/lower_time.py +++ b/artiq/transforms/lower_time.py @@ -1,15 +1,15 @@ """ -This transform implements time management functions (delay/now/at) +This transform implements time management functions (delay_mu/now_mu/at_mu) using an accumulator 'now' and simple replacement rules: - delay(t) -> now += t - now() -> now - at(t) -> now = t + delay_mu(t) -> now += t + now_mu() -> now + at_mu(t) -> now = t -Time parameters must be quantized to integers before running this transform. +The function delay(), that uses seconds, must be lowered to delay_mu() before +invoking this transform. The accumulator is initialized to an int64 value at the beginning of the output function. - """ import ast @@ -17,7 +17,7 @@ import ast class _TimeLowerer(ast.NodeTransformer): def visit_Call(self, node): - if node.func.id == "now": + if node.func.id == "now_mu": return ast.copy_location(ast.Name("now", ast.Load()), node) else: self.generic_visit(node) @@ -27,13 +27,13 @@ class _TimeLowerer(ast.NodeTransformer): r = node if isinstance(node.value, ast.Call): funcname = node.value.func.id - if funcname == "delay": + if funcname == "delay_mu": r = ast.copy_location( ast.AugAssign(target=ast.Name("now", ast.Store()), op=ast.Add(), value=node.value.args[0]), node) - elif funcname == "at": + elif funcname == "at_mu": r = ast.copy_location( ast.Assign(targets=[ast.Name("now", ast.Store())], value=node.value.args[0]), diff --git a/artiq/transforms/lower_units.py b/artiq/transforms/lower_units.py deleted file mode 100644 index eef71e564..000000000 --- a/artiq/transforms/lower_units.py +++ /dev/null @@ -1,190 +0,0 @@ -import ast -from collections import defaultdict -from copy import copy - -from artiq.language import units -from artiq.transforms.tools import embeddable_func_names - - -def _add_units(f, unit_list): - def wrapper(*args): - new_args = [] - for arg, unit in zip(args, unit_list): - if unit is None: - new_args.append(arg) - else: - if isinstance(arg, list): - new_args.append([units.Quantity(x, unit) for x in arg]) - else: - new_args.append(units.Quantity(arg, unit)) - return f(*new_args) - return wrapper - - -class _UnitsLowerer(ast.NodeTransformer): - def __init__(self, rpc_map): - self.rpc_map = rpc_map - # (original rpc number, (unit list)) -> new rpc number - self.rpc_remap = defaultdict(lambda: len(self.rpc_remap)) - self.variable_units = dict() - - def visit_Name(self, node): - try: - unit = self.variable_units[node.id] - except KeyError: - pass - else: - if unit is not None: - node.unit = unit - return node - - def visit_BoolOp(self, node): - self.generic_visit(node) - us = [getattr(value, "unit", None) for value in node.values] - if not all(u == us[0] for u in us[1:]): - raise units.DimensionError - return node - - def visit_Compare(self, node): - self.generic_visit(node) - u0 = getattr(node.left, "unit", None) - us = [getattr(comparator, "unit", None) - for comparator in node.comparators] - if not all(u == u0 for u in us): - raise units.DimensionError - return node - - def visit_UnaryOp(self, node): - self.generic_visit(node) - if hasattr(node.operand, "unit"): - node.unit = node.operand.unit - return node - - def visit_BinOp(self, node): - self.generic_visit(node) - op = type(node.op) - left_unit = getattr(node.left, "unit", None) - right_unit = getattr(node.right, "unit", None) - if op in (ast.Add, ast.Sub, ast.Mod): - unit = units.addsub_dimension(left_unit, right_unit) - elif op == ast.Mult: - unit = units.mul_dimension(left_unit, right_unit) - elif op in (ast.Div, ast.FloorDiv): - unit = units.div_dimension(left_unit, right_unit) - else: - if left_unit is not None or right_unit is not None: - raise units.DimensionError - unit = None - if unit is not None: - node.unit = unit - return node - - def visit_Attribute(self, node): - self.generic_visit(node) - if node.attr == "amount" and hasattr(node.value, "unit"): - del node.value.unit - return node.value - else: - return node - - def visit_List(self, node): - self.generic_visit(node) - if node.elts: - us = [getattr(elt, "unit", None) for elt in node.elts] - if not all(u == us[0] for u in us[1:]): - raise units.DimensionError - node.unit = us[0] - return node - - def visit_ListComp(self, node): - self.generic_visit(node) - if hasattr(node.elt, "unit"): - node.unit = node.elt.unit - return node - - def visit_Call(self, node): - self.generic_visit(node) - if node.func.id == "Quantity": - amount, unit = node.args - amount.unit = unit.s - return amount - elif node.func.id in ("now", "cycles_to_time"): - node.unit = "s" - elif node.func.id == "syscall": - # only RPCs can have units - if node.args[0].s == "rpc": - unit_list = tuple(getattr(arg, "unit", None) - for arg in node.args[2:]) - rpc_n = node.args[1].n - node.args[1].n = self.rpc_remap[(rpc_n, (unit_list))] - else: - if any(hasattr(arg, "unit") for arg in node.args): - raise units.DimensionError - elif node.func.id in ("delay", "at", "time_to_cycles", "watchdog"): - if getattr(node.args[0], "unit", None) != "s": - raise units.DimensionError - elif node.func.id == "check_unit": - self.generic_visit(node) - elif node.func.id in embeddable_func_names: - # must be last (some embeddable funcs may have units) - if any(hasattr(arg, "unit") for arg in node.args): - raise units.DimensionError - return node - - def visit_Expr(self, node): - self.generic_visit(node) - if (isinstance(node.value, ast.Call) - and node.value.func.id == "check_unit"): - call = node.value - if (isinstance(call.args[1], ast.NameConstant) - and call.args[1].value is None): - if hasattr(call.value.args[0], "unit"): - raise units.DimensionError - elif isinstance(call.args[1], ast.Str): - if getattr(call.args[0], "unit", None) != call.args[1].s: - raise units.DimensionError - else: - raise NotImplementedError - return None - else: - return node - - def _update_target(self, target, unit): - if isinstance(target, ast.Name): - if target.id in self.variable_units: - if self.variable_units[target.id] != unit: - raise TypeError( - "Inconsistent units for variable '{}': '{}' and '{}'" - .format(target.id, - self.variable_units[target.id], - unit)) - else: - self.variable_units[target.id] = unit - - def visit_Assign(self, node): - node.value = self.visit(node.value) - unit = getattr(node.value, "unit", None) - for target in node.targets: - self._update_target(target, unit) - return node - - def visit_AugAssign(self, node): - value = self.visit_BinOp(ast.BinOp( - op=node.op, left=node.target, right=node.value)) - unit = getattr(value, "unit", None) - self._update_target(node.target, unit) - return node - - # Only dimensionless iterators are supported - def visit_For(self, node): - self.generic_visit(node) - self._update_target(node.target, None) - return node - - -def lower_units(func_def, rpc_map): - ul = _UnitsLowerer(rpc_map) - ul.visit(func_def) - original_map = copy(rpc_map) - for (original_rpcn, unit_list), new_rpcn in ul.rpc_remap.items(): - rpc_map[new_rpcn] = _add_units(original_map[original_rpcn], unit_list) diff --git a/artiq/transforms/quantize_time.py b/artiq/transforms/quantize_time.py index 835dcd780..aad75069a 100644 --- a/artiq/transforms/quantize_time.py +++ b/artiq/transforms/quantize_time.py @@ -1,12 +1,12 @@ """ -This transform turns calls to delay/now/at that use non-integer time -expressed in seconds into calls that use int64 time expressed in multiples of -ref_period. +This transform turns calls to delay() that use non-integer time +expressed in seconds into calls to delay_mu() that use int64 time +expressed in multiples of ref_period. It does so by inserting multiplication/division/rounding operations around those calls. -The time_to_cycles and cycles_to_time core language functions are also +The seconds_to_mu and mu_to_seconds core language functions are also implemented here, as well as watchdog to syscall conversion. """ @@ -15,14 +15,7 @@ import ast from artiq.transforms.tools import value_to_ast -def _call_now(node): - return ast.copy_location( - ast.Call(func=ast.Name("now", ast.Load()), - args=[], keywords=[], starargs=[], kwargs=[]), - node) - - -def _time_to_cycles(ref_period, node): +def _seconds_to_mu(ref_period, node): divided = ast.copy_location( ast.BinOp(left=node, op=ast.Div(), @@ -35,7 +28,7 @@ def _time_to_cycles(ref_period, node): divided) -def _cycles_to_time(ref_period, node): +def _mu_to_seconds(ref_period, node): return ast.copy_location( ast.BinOp(left=node, op=ast.Mult(), @@ -50,29 +43,22 @@ class _TimeQuantizer(ast.NodeTransformer): def visit_Call(self, node): funcname = node.func.id - if funcname == "now": - return _cycles_to_time(self.ref_period, _call_now(node)) - elif funcname == "delay" or funcname == "at": + if funcname == "delay": + node.func.id = "delay_mu" if (isinstance(node.args[0], ast.Call) - and node.args[0].func.id == "cycles_to_time"): + and node.args[0].func.id == "mu_to_seconds"): # optimize: - # delay/at(cycles_to_time(x)) -> delay/at(x) + # delay(mu_to_seconds(x)) -> delay_mu(x) node.args[0] = self.visit(node.args[0].args[0]) else: - node.args[0] = _time_to_cycles(self.ref_period, - self.visit(node.args[0])) + node.args[0] = _seconds_to_mu(self.ref_period, + self.visit(node.args[0])) return node - elif funcname == "time_to_cycles": - if (isinstance(node.args[0], ast.Call) - and node.args[0].func.id == "now"): - # optimize: - # time_to_cycles(now()) -> now() - return _call_now(node) - else: - return _time_to_cycles(self.ref_period, + elif funcname == "seconds_to_mu": + return _seconds_to_mu(self.ref_period, self.visit(node.args[0])) - elif funcname == "cycles_to_time": - return _cycles_to_time(self.ref_period, + elif funcname == "mu_to_seconds": + return _mu_to_seconds(self.ref_period, self.visit(node.args[0])) else: self.generic_visit(node) @@ -123,6 +109,5 @@ class _TimeQuantizer(ast.NodeTransformer): return node - def quantize_time(func_def, ref_period): _TimeQuantizer(ref_period).visit(func_def) diff --git a/artiq/transforms/tools.py b/artiq/transforms/tools.py index d9daf838f..97d596d2b 100644 --- a/artiq/transforms/tools.py +++ b/artiq/transforms/tools.py @@ -6,12 +6,13 @@ from artiq.language import units embeddable_funcs = ( - core_language.delay, core_language.at, core_language.now, - core_language.time_to_cycles, core_language.cycles_to_time, + core_language.delay_mu, core_language.at_mu, core_language.now_mu, + core_language.delay, + core_language.seconds_to_mu, core_language.mu_to_seconds, core_language.syscall, core_language.watchdog, range, bool, int, float, round, len, core_language.int64, core_language.round64, - Fraction, units.Quantity, units.check_unit, core_language.EncodedException + Fraction, core_language.EncodedException ) embeddable_func_names = {func.__name__ for func in embeddable_funcs} @@ -61,11 +62,6 @@ def value_to_ast(value): for kg in core_language.kernel_globals: if value is getattr(core_language, kg): return ast.Name(kg, ast.Load()) - if isinstance(value, units.Quantity): - return ast.Call( - func=ast.Name("Quantity", ast.Load()), - args=[value_to_ast(value.amount), ast.Str(value.unit)], - keywords=[], starargs=None, kwargs=None) raise NotASTRepresentable(str(value)) @@ -88,14 +84,6 @@ def eval_constant(node): numerator = eval_constant(node.args[0]) denominator = eval_constant(node.args[1]) return Fraction(numerator, denominator) - elif funcname == "Quantity": - amount, unit = node.args - amount = eval_constant(amount) - try: - unit = getattr(units, unit.id) - except: - raise NotConstant - return units.Quantity(amount, unit) else: raise NotConstant else: @@ -105,8 +93,7 @@ def eval_constant(node): _replaceable_funcs = { "bool", "int", "float", "round", "int64", "round64", "Fraction", - "time_to_cycles", "cycles_to_time", - "Quantity" + "seconds_to_mu", "mu_to_seconds" } diff --git a/artiq/wavesynth/coefficients.py b/artiq/wavesynth/coefficients.py index f0f489359..ee9efb351 100644 --- a/artiq/wavesynth/coefficients.py +++ b/artiq/wavesynth/coefficients.py @@ -5,11 +5,19 @@ from scipy.special import binom class UnivariateMultiSpline: """Multidimensional wrapper around `scipy.interpolate.sp*` functions. - `scipy.inteprolate.splprep` unfortunately does only up to 12 dimsions. + `scipy.inteprolate.splprep` is limited to 12 dimensions. """ - def __init__(self, x, y, order=4): + def __init__(self, x, y, *, x0=None, order=4, **kwargs): self.order = order - self.s = [splrep(x, yi, k=order - 1) for yi in y] + self.x = x + self.s = [] + for i, yi in enumerate(y): + if x0 is not None: + yi = self.upsample_knots(x0[i], yi, x) + self.s.append(splrep(x, yi, k=order - 1, **kwargs)) + + def upsample_knots(self, x0, y0, x): + return splev(x, splrep(x0, y0, k=self.order - 1)) def lev(self, x, *a, **k): return np.array([splev(x, si) for si in self.s]) @@ -30,20 +38,6 @@ class UnivariateMultiSpline: return np.array([self.lev(x, der=i) for i in range(self.order)]) -class UnivariateMultiSparseSpline(UnivariateMultiSpline): - def __init__(self, d, x0=None, order=4): - self.order = order - self.n = sorted(set(n for x, n, y in d)) - self.s = [] - for n in self.n: - x, y = np.array([(x, y) for x, ni, y in d if n == ni]).T - if x0 is not None: - y0 = splev(x0, splrep(x, y, k=order - 1)) - x, y = x0, y0 - s = splrep(x, y, k=order - 1) - self.s.append(s) - - def pad_const(x, n, axis=0): """Prefix and postfix the array `x` by `n` repetitions of the first and last value along `axis`. @@ -58,28 +52,28 @@ def pad_const(x, n, axis=0): def build_segment(durations, coefficients, target="bias", - variable="amplitude"): + variable="amplitude", compress=True): """Build a wavesynth-style segment from homogeneous duration and coefficient data. :param durations: 1D sequence of line durations. - :param coefficients: 3D array with shape `(n, m, len(x))`, + :param coefficients: 3D array with shape `(n, m, len(durations))`, with `n` being the interpolation order + 1 and `m` the number of channels. :param target: The target component of the channel to affect. :param variable: The variable within the target component. + :param compress: If `True`, skip zero high order coefficients. """ for dxi, yi in zip(durations, coefficients.transpose()): - d = {"duration": int(dxi)} - d["channel_data"] = cd = [] + cd = [] for yij in yi: cdj = [] for yijk in reversed(yij): - if cdj or abs(yijk): + if cdj or abs(yijk) or not compress: cdj.append(float(yijk)) cdj.reverse() cd.append({target: {variable: cdj}}) - yield d + yield {"duration": int(dxi), "channel_data": cd} class CoefficientSource: @@ -125,7 +119,7 @@ class CoefficientSource: with `n` being the number of channels.""" raise NotImplementedError - def get_segment_data(self, start, stop, scale, cutoff=1e-12, + def get_segment_data(self, start, stop, scale, *, cutoff=1e-12, target="bias", variable="amplitude"): """Build wavesynth segment data. @@ -154,7 +148,7 @@ class CoefficientSource: """ for i, line in enumerate(self.get_segment_data(*args, **kwargs)): if i == 0: - line["trigger"] = True + line["trigger"] = trigger segment.add_line(**line) @@ -176,7 +170,7 @@ class SplineSource(CoefficientSource): self.y = pad_const(self.y, order, axis=1) assert self.y.shape[1] == self.x.shape[0] - self.spline = UnivariateMultiSpline(self.x, self.y, order) + self.spline = UnivariateMultiSpline(self.x, self.y, order=order) def crop_x(self, start, stop): ia, ib = np.searchsorted(self.x, (start, stop)) @@ -187,7 +181,8 @@ class SplineSource(CoefficientSource): return np.r_[start, x, stop] def scale_x(self, x, scale, min_duration=1, min_length=20): - """ + """Enforce, round, and scale x to device-dependent values. + Due to minimum duration and/or minimum segment length constraints this method may drop samples from `x_sample` or adjust `durations` to comply. But `x_sample` and `durations` should be kept consistent. @@ -235,14 +230,14 @@ class ComposingSplineSource(SplineSource): self.y = pad_const(self.y, order, axis=2) assert self.y.shape[2] == self.x.shape[0] - self.splines = [UnivariateMultiSpline(self.x, yi, order) + self.splines = [UnivariateMultiSpline(self.x, yi, order=order) for yi in self.y] # need to resample/upsample the shim splines to the master spline knots # shim knot spacings can span an master spline knot and thus would # cross a highest order derivative boundary - self.components = UnivariateMultiSparseSpline( - components, self.x, order) + y0, x0 = zip(*components) + self.components = UnivariateMultiSpline(self.x, y0, x0=x0, order=order) def __call__(self, t, gain={}, offset={}): der = list((set(self.components.n) | set(offset)) diff --git a/artiq/wavesynth/compute_samples.py b/artiq/wavesynth/compute_samples.py index 0658f8bde..476fb385a 100644 --- a/artiq/wavesynth/compute_samples.py +++ b/artiq/wavesynth/compute_samples.py @@ -9,6 +9,8 @@ class Spline: self.c = [0.0] def set_coefficients(self, c): + if not c: + c = [0.] self.c = copy(c) discrete_compensate(self.c) @@ -25,6 +27,8 @@ class SplinePhase: self.c0 = 0.0 def set_coefficients(self, c): + if not c: + c = [0.] self.c0 = c[0] c1p = c[1:] discrete_compensate(c1p) diff --git a/benchmarks/all.py b/benchmarks/all.py deleted file mode 100644 index d0a34263b..000000000 --- a/benchmarks/all.py +++ /dev/null @@ -1,18 +0,0 @@ -from artiq import * - -import pulse_rate, rtio_skew, rpc_timing - - -_exps = [pulse_rate.PulseRate, rtio_skew.RTIOSkew, rpc_timing.RPCTiming] - -class AllBenchmarks(Experiment, AutoDB): - """All benchmarks""" - - def build(self): - self.se = [] - for exp in _exps: - self.se.append(exp(self.dbh)) - - def run(self): - for se in self.se: - se.run() diff --git a/benchmarks/ddb.pyon b/benchmarks/ddb.pyon deleted file mode 100644 index 8dd20362c..000000000 --- a/benchmarks/ddb.pyon +++ /dev/null @@ -1,28 +0,0 @@ -{ - "comm": { - "type": "local", - "module": "artiq.coredevice.comm_tcp", - "class": "Comm", - "arguments": {"host": "192.168.0.42"} - }, - "core": { - "type": "local", - "module": "artiq.coredevice.core", - "class": "Core", - "arguments": {} - }, - - "pmt0": { - "type": "local", - "module": "artiq.coredevice.ttl", - "class": "TTLInOut", - "arguments": {"channel": 0} - }, - - "ttl0": { - "type": "local", - "module": "artiq.coredevice.ttl", - "class": "TTLOut", - "arguments": {"channel": 2} - }, -} diff --git a/benchmarks/pdb.pyon b/benchmarks/pdb.pyon deleted file mode 100644 index 0967ef424..000000000 --- a/benchmarks/pdb.pyon +++ /dev/null @@ -1 +0,0 @@ -{} diff --git a/benchmarks/pulse_rate.py b/benchmarks/pulse_rate.py deleted file mode 100644 index 4de78fa06..000000000 --- a/benchmarks/pulse_rate.py +++ /dev/null @@ -1,26 +0,0 @@ -from artiq import * -from artiq.coredevice.runtime_exceptions import RTIOUnderflow - - -class PulseRate(Experiment, AutoDB): - """Sustained pulse rate""" - - class DBKeys: - core = Device() - ttl0 = Device() - pulse_rate = Result() - - @kernel - def run(self): - T = time_to_cycles(100*ns) - while True: - try: - for i in range(1000): - self.ttl0.pulse(cycles_to_time(T)) - delay(cycles_to_time(T)) - except RTIOUnderflow: - T += 1 - self.core.break_realtime() - else: - self.pulse_rate = cycles_to_time(2*T) - break diff --git a/benchmarks/rpc_timing.py b/benchmarks/rpc_timing.py deleted file mode 100644 index b14da820a..000000000 --- a/benchmarks/rpc_timing.py +++ /dev/null @@ -1,32 +0,0 @@ -from math import sqrt - -from artiq import * - - -class RPCTiming(Experiment, AutoDB): - """RPC timing""" - - class DBKeys: - core = Device() - repeats = Argument(100) - rpc_time_mean = Result() - rpc_time_stddev = Result() - - def nop(self, x): - pass - - @kernel - def bench(self): - self.ts = [0.0 for _ in range(self.repeats)] - for i in range(self.repeats): - t1 = self.core.get_rtio_time() - self.nop(10) - t2 = self.core.get_rtio_time() - self.ts[i] = float(t2.amount - t1.amount) - - def run(self): - self.bench() - mean = sum(self.ts)/self.repeats - self.rpc_time_stddev = sqrt( - sum([(t - mean)**2 for t in self.ts])/self.repeats)*s - self.rpc_time_mean = mean*s diff --git a/benchmarks/rtio_skew.py b/benchmarks/rtio_skew.py deleted file mode 100644 index a5770864b..000000000 --- a/benchmarks/rtio_skew.py +++ /dev/null @@ -1,29 +0,0 @@ -from artiq import * - - -class PulseNotReceived(Exception): - pass - - -class RTIOSkew(Experiment, AutoDB): - """RTIO skew""" - - class DBKeys: - core = Device() - pmt0 = Device() - rtio_skew = Result() - - @kernel - def run(self): - self.pmt0.output() - delay(1*us) - with parallel: - self.pmt0.gate_rising(10*us) - with sequential: - delay(5*us) - out_t = now() - self.pmt0.pulse(5*us) - in_t = self.pmt0.timestamp() - if in_t < 0*s: - raise PulseNotReceived - self.rtio_skew = out_t - in_t diff --git a/conda/artiq/build.sh b/conda/artiq/build.sh index e6c049b18..c3b7694f0 100755 --- a/conda/artiq/build.sh +++ b/conda/artiq/build.sh @@ -22,22 +22,30 @@ cd $SRC_DIR/misoc; python make.py -X ../soc -t artiq_kc705 build-headers build-b make -C soc/runtime clean runtime.fbi cd $SRC_DIR/misoc; python make.py -X ../soc -t artiq_kc705 $MISOC_EXTRA_VIVADO_CMDLINE build-bitstream; cd - +# install KC705 binaries + +cp soc/runtime/runtime.fbi $BIN_PREFIX/kc705/ +cp $SRC_DIR/misoc/software/bios/bios.bin $BIN_PREFIX/kc705/ +cp $SRC_DIR/misoc/build/artiq_kc705-nist_qc1-kc705.bit $BIN_PREFIX/kc705/ +wget http://sionneau.net/artiq/binaries/kc705/flash_proxy/bscan_spi_kc705.bit +mv bscan_spi_kc705.bit $BIN_PREFIX/kc705/ + # build for Pipistrello cd $SRC_DIR/misoc; python make.py -X ../soc -t artiq_pipistrello build-headers build-bios; cd - make -C soc/runtime clean runtime.fbi cd $SRC_DIR/misoc; python make.py -X ../soc -t artiq_pipistrello $MISOC_EXTRA_ISE_CMDLINE build-bitstream; cd - -# install KC705 binaries - -cp soc/runtime/runtime.fbi $BIN_PREFIX/kc705/ -cp $SRC_DIR/misoc/software/bios/bios.bin $BIN_PREFIX/kc705/ -cp $SRC_DIR/misoc/build/artiq_kc705-nist_qc1-kc705.bin $BIN_PREFIX/kc705/ - # install Pipistrello binaries cp soc/runtime/runtime.fbi $BIN_PREFIX/pipistrello/ cp $SRC_DIR/misoc/software/bios/bios.bin $BIN_PREFIX/pipistrello/ -cp $SRC_DIR/misoc/build/artiq_pipistrello-nist_qc1-pipistrello.bin $BIN_PREFIX/pipistrello/ +cp $SRC_DIR/misoc/build/artiq_pipistrello-nist_qc1-pipistrello.bit $BIN_PREFIX/pipistrello/ +wget http://www.phys.ethz.ch/~robertjo/bscan_spi_lx45_csg324.bit +mv bscan_spi_lx45_csg324.bit $BIN_PREFIX/pipistrello/ cp artiq/frontend/artiq_flash.sh $PREFIX/bin + +# misc +cp misc/99-papilio.rules $ARTIQ_PREFIX/misc/ +cp misc/99-kc705.rules $ARTIQ_PREFIX/misc/ diff --git a/conda/artiq/meta.yaml b/conda/artiq/meta.yaml index 8322af1af..97c9a5e51 100644 --- a/conda/artiq/meta.yaml +++ b/conda/artiq/meta.yaml @@ -11,8 +11,11 @@ build: entry_points: - artiq_client = artiq.frontend.artiq_client:main - artiq_compile = artiq.frontend.artiq_compile:main + - artiq_coreconfig = artiq.frontend.artiq_coreconfig:main - artiq_ctlmgr = artiq.frontend.artiq_ctlmgr:main + - artiq_gui = artiq.frontend.artiq_gui:main - artiq_master = artiq.frontend.artiq_master:main + - artiq_mkfs = artiq.frontend.artiq_mkfs:main - artiq_rpctool = artiq.frontend.artiq_rpctool:main - artiq_run = artiq.frontend.artiq_run:main - lda_controller = artiq.frontend.lda_controller:main @@ -21,17 +24,16 @@ build: - pdq2_controller = artiq.frontend.pdq2_controller:main - pxi6733_controller = artiq.frontend.pxi6733_controller:main - thorlabs_tcube_controller = artiq.frontend.thorlabs_tcube_controller:main - - artiq_gui = artiq.frontend.artiq_gui:main requirements: build: - - python + - python >=3.4.3 - setuptools - numpy - migen - pyelftools run: - - python + - python >=3.4.3 - llvmlite-or1k - scipy - numpy @@ -45,6 +47,7 @@ requirements: - pyelftools - quamash - pyqtgraph + - flterm # [linux] test: imports: diff --git a/conda/cairo/build.sh b/conda/cairo/build.sh deleted file mode 100644 index f390466ca..000000000 --- a/conda/cairo/build.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash -export CFLAGS="-I$PREFIX/include -L$PREFIX/lib" -./configure \ ---prefix=$PREFIX \ ---disable-static \ ---enable-warnings \ ---enable-ft \ ---enable-ps \ ---enable-pdf \ ---enable-svg \ ---disable-gtk-doc -make -make install -rm -rf $PREFIX/share -# vim:set ts=8 sw=4 sts=4 tw=78 et: diff --git a/conda/cairo/meta.yaml b/conda/cairo/meta.yaml deleted file mode 100644 index 001dd9c87..000000000 --- a/conda/cairo/meta.yaml +++ /dev/null @@ -1,25 +0,0 @@ -# This conda recipe is based on this one: https://github.com/tpn/trent-conda-recipes/tree/master/cairo - -package: - name: cairo - version: 1.14.0 -source: - fn: cairo-1.14.0.tar.xz - url: http://cairographics.org/releases/cairo-1.14.0.tar.xz -build: - number: 0 -requirements: - build: - - freetype - - libpng - - pixman >=0.30 - - zlib - run: - - freetype - - libpng - - pixman >=0.30 - - zlib -about: - home: http://cairographics.org/ - license: LGPL 2.1 and MPL 1.1 -# vim:set ts=8 sw=2 sts=2 tw=78 et: diff --git a/conda/cairoplot3-artiq/meta.yaml b/conda/cairoplot3-artiq/meta.yaml deleted file mode 100644 index 00a171fa6..000000000 --- a/conda/cairoplot3-artiq/meta.yaml +++ /dev/null @@ -1,29 +0,0 @@ -package: - name: cairoplot3-artiq - version: "3.1.2" - -source: - git_url: https://github.com/m-labs/cairoplot3 - git_tag: master - -build: - number: 0 - script: $PYTHON setup.py install - -requirements: - build: - - python - - setuptools - run: - - python - - pycairo - -test: - imports: - - cairoplot - - series - -about: - home: https://github.com/m-labs/cairoplot3 - license: LGPL - summary: 'Module to easily plot beautiful graphics using PyCairo' diff --git a/conda/flterm/build.sh b/conda/flterm/build.sh new file mode 100644 index 000000000..1121beb65 --- /dev/null +++ b/conda/flterm/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +make -C $SRC_DIR/tools flterm +mkdir -p $PREFIX/bin +cp $SRC_DIR/tools/flterm $PREFIX/bin/ diff --git a/conda/flterm/meta.yaml b/conda/flterm/meta.yaml new file mode 100644 index 000000000..16afb47cb --- /dev/null +++ b/conda/flterm/meta.yaml @@ -0,0 +1,12 @@ +package: + name: flterm + version: 0 + +source: + git_url: https://github.com/m-labs/misoc + git_tag: master + +about: + home: https://github.com/m-labs/misoc/blob/master/tools/flterm.c + license: 3-clause BSD + summary: 'Serial terminal to connect to MiSoC uart.' diff --git a/conda/gbulb-artiq/meta.yaml b/conda/gbulb-artiq/meta.yaml deleted file mode 100644 index 20baf2e8e..000000000 --- a/conda/gbulb-artiq/meta.yaml +++ /dev/null @@ -1,28 +0,0 @@ -package: - name: gbulb-artiq - version: "0.1" - -source: - git_url: https://github.com/m-labs/gbulb - git_tag: master - -build: - number: 0 - script: $PYTHON setup.py install - -requirements: - build: - - python - - setuptools - run: - - python - - pygobject - -test: - imports: - - gbulb - -about: - home: https://github.com/m-labs/gbulb - license: Apache 2.0 - summary: 'GLib event loop for tulip (PEP 3156)' diff --git a/conda/llvmdev-or1k/meta.yaml b/conda/llvmdev-or1k/meta.yaml index 87586bc24..9406b8943 100644 --- a/conda/llvmdev-or1k/meta.yaml +++ b/conda/llvmdev-or1k/meta.yaml @@ -12,6 +12,7 @@ build: requirements: build: - system [linux and not armv6] + - cmake [linux] run: - system [linux and not armv6] diff --git a/conda/llvmlite-or1k/bld.bat b/conda/llvmlite-or1k/bld.bat index 78ab6ac38..17e63ad30 100644 --- a/conda/llvmlite-or1k/bld.bat +++ b/conda/llvmlite-or1k/bld.bat @@ -6,6 +6,8 @@ if exist ffi\build rmdir /S /Q ffi\build @rem Apply patches patch -p1 < %RECIPE_DIR%/../../misc/llvmlite-add-all-targets.patch +patch -p1 < %RECIPE_DIR%/../../misc/llvmlite-rename.patch +patch -p1 < %RECIPE_DIR%/../../misc/llvmlite-build-as-debug-on-windows.patch %PYTHON% -S setup.py install if errorlevel 1 exit 1 diff --git a/conda/llvmlite-or1k/build.sh b/conda/llvmlite-or1k/build.sh index f2734bba4..327c15518 100755 --- a/conda/llvmlite-or1k/build.sh +++ b/conda/llvmlite-or1k/build.sh @@ -1,4 +1,6 @@ #!/bin/bash patch -p1 < ${RECIPE_DIR}/../../misc/llvmlite-add-all-targets.patch +patch -p1 < ${RECIPE_DIR}/../../misc/llvmlite-rename.patch +patch -p1 < ${RECIPE_DIR}/../../misc/llvmlite-build-as-debug-on-windows.patch PATH=/usr/local/llvm-or1k/bin:$PATH $PYTHON setup.py install diff --git a/conda/llvmlite-or1k/meta.yaml b/conda/llvmlite-or1k/meta.yaml index 9dbd8ab83..db3c24bcd 100644 --- a/conda/llvmlite-or1k/meta.yaml +++ b/conda/llvmlite-or1k/meta.yaml @@ -15,12 +15,12 @@ requirements: - python build: - number: 0 + number: 1 test: imports: - - llvmlite - - llvmlite.llvmpy + - llvmlite_or1k + - llvmlite_or1k.llvmpy about: home: https://pypi.python.org/pypi/llvmlite/ diff --git a/conda/pycairo/bld.bat b/conda/pycairo/bld.bat deleted file mode 100644 index 39b5e1fee..000000000 --- a/conda/pycairo/bld.bat +++ /dev/null @@ -1 +0,0 @@ -%PYTHON% setup.py install diff --git a/conda/pycairo/build.sh b/conda/pycairo/build.sh deleted file mode 100755 index 8e25a1455..000000000 --- a/conda/pycairo/build.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -$PYTHON setup.py install diff --git a/conda/pycairo/meta.yaml b/conda/pycairo/meta.yaml deleted file mode 100644 index ae7cccea6..000000000 --- a/conda/pycairo/meta.yaml +++ /dev/null @@ -1,29 +0,0 @@ -package: - name: pycairo - version: "1.10.0" - -source: - git_url: git://git.cairographics.org/git/pycairo - git_tag: master - -build: - number: 2 - script: $PYTHON setup.py install - -requirements: - build: - - python - - setuptools - - cairo 1.14.0 # [not win] - run: - - python - - cairo 1.14.0 # [not win] - -test: - imports: - - cairo - -about: - home: http://cairographics.org/pycairo/ - license: LGPL 3 - summary: 'Python 3 bindings for cairo' diff --git a/conda/pygobject/build.sh b/conda/pygobject/build.sh deleted file mode 100755 index 74249fff2..000000000 --- a/conda/pygobject/build.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -TERM=xterm ./autogen.sh -export CFLAGS="-L$PREFIX/lib -I$PREFIX/include" -./configure --prefix=$PREFIX -make -make install diff --git a/conda/pygobject/meta.yaml b/conda/pygobject/meta.yaml deleted file mode 100644 index 4fcc319f0..000000000 --- a/conda/pygobject/meta.yaml +++ /dev/null @@ -1,30 +0,0 @@ -package: - name: pygobject - version: "3.14.0" - -source: - git_url: git://git.gnome.org/pygobject - git_tag: 3.14.0 - -build: - number: 0 - -requirements: - build: - - python - run: - - python - -test: - imports: - - gi - - gi.repository - - gi.repository.GObject - - gi.repository.GLib - - gi.repository.Gio - - gi.repository.Gtk - -about: - home: https://wiki.gnome.org/action/show/Projects/PyGObject?action=show&redirect=PyGObject - license: LGPL 2.1 - summary: 'PyGObject is a Python extension module that gives clean and consistent access to the entire GNOME software platform through the use of GObject Introspection.' diff --git a/conda/pyqtgraph/meta.yaml b/conda/pyqtgraph/meta.yaml index 4f9939dfe..9be6a6dda 100644 --- a/conda/pyqtgraph/meta.yaml +++ b/conda/pyqtgraph/meta.yaml @@ -1,11 +1,10 @@ package: name: pyqtgraph - version: "0.9.10" + version: 0.9.10~a6d5e28 source: - fn: pyqtgraph-0.9.10.tar.gz - url: https://pypi.python.org/packages/source/p/pyqtgraph/pyqtgraph-0.9.10.tar.gz - md5: bd84bf7537c43cf38db81cc1ad4f743a + git_url: https://github.com/pyqtgraph/pyqtgraph.git + git_rev: a6d5e28 requirements: build: diff --git a/doc/manual/conf.py b/doc/manual/conf.py index e6d0afc43..2df182943 100644 --- a/doc/manual/conf.py +++ b/doc/manual/conf.py @@ -25,7 +25,7 @@ class Mock(MagicMock): return Mock() -mock_modules = ["quamash", "pyqtgraph", "matplotlib"] +mock_modules = ["artiq.gui.moninj", "quamash", "pyqtgraph", "matplotlib"] for module in mock_modules: sys.modules[module] = Mock() diff --git a/doc/manual/core_device_flash_storage.rst b/doc/manual/core_device_flash_storage.rst new file mode 100644 index 000000000..fb2bb8d9a --- /dev/null +++ b/doc/manual/core_device_flash_storage.rst @@ -0,0 +1,14 @@ +.. _core-device-flash-storage: + +Core device flash storage +========================= + +The core device contains some flash space that can be used to store +some configuration data. + +This storage area is used to store the core device MAC address, IP address and even the idle kernel. + +The flash storage area is one sector (64 kB) large and is organized as a list +of key-value records. + +This flash storage space can be accessed by using the artiq_coreconfig.py :ref:`core-device-configuration-tool`. diff --git a/doc/manual/core_drivers_reference.rst b/doc/manual/core_drivers_reference.rst index 0c9c38301..56fca095b 100644 --- a/doc/manual/core_drivers_reference.rst +++ b/doc/manual/core_drivers_reference.rst @@ -12,6 +12,9 @@ These drivers are for peripherals closely integrated into the core device, which :mod:`artiq.coredevice.dds` module ---------------------------------- +.. autoclass:: artiq.coredevice.dds._DDSGeneric + :members: + .. automodule:: artiq.coredevice.dds :members: diff --git a/doc/manual/core_language_reference.rst b/doc/manual/core_language_reference.rst index 35c285e01..9f0a21d06 100644 --- a/doc/manual/core_language_reference.rst +++ b/doc/manual/core_language_reference.rst @@ -9,10 +9,10 @@ The most commonly used features from those modules can be imported with ``from a .. automodule:: artiq.language.core :members: -:mod:`artiq.language.db` module -------------------------------- +:mod:`artiq.language.environment` module +---------------------------------------- -.. automodule:: artiq.language.db +.. automodule:: artiq.language.environment :members: :mod:`artiq.language.units` module diff --git a/doc/manual/default_network_ports.rst b/doc/manual/default_network_ports.rst index 10b54cf23..c5728e428 100644 --- a/doc/manual/default_network_ports.rst +++ b/doc/manual/default_network_ports.rst @@ -4,7 +4,9 @@ Default network ports +--------------------------+--------------+ | Component | Default port | +==========================+==============+ -| Core device | 1381 | +| Core device (main) | 1381 | ++--------------------------+--------------+ +| Core device (mon/inj) | 3250 (UDP) | +--------------------------+--------------+ | Master (notifications) | 3250 | +--------------------------+--------------+ diff --git a/doc/manual/developing_a_ndsp.rst b/doc/manual/developing_a_ndsp.rst index 98e24e15e..d6320c242 100644 --- a/doc/manual/developing_a_ndsp.rst +++ b/doc/manual/developing_a_ndsp.rst @@ -103,7 +103,7 @@ Run it as before, while the controller is running. You should see the message ap $ ./hello_controller.py message: Hello World! -When using the driver in an experiment, for simple cases the ``Client`` instance can be returned by the :class:`artiq.language.db.AutoDB` mechanism and used normally as a device. +When using the driver in an experiment, the ``Client`` instance can be returned by the environment mechanism (via the ``get_device`` and ``attr_device`` methods of :class:`artiq.language.environment.HasEnvironment`) and used normally as a device. :warning: RPC servers operate on copies of objects provided by the client, and modifications to mutable types are not written back. For example, if the client passes a list as a parameter of an RPC method, and that method ``append()s`` an element to the list, the element is not appended to the client's list. @@ -177,7 +177,7 @@ General guidelines * Use new-style formatting (``str.format``) except for logging where it is not well supported, and double quotes for strings. * The device identification (e.g. serial number, or entry in ``/dev``) to attach to must be passed as a command-line parameter to the controller. We suggest using ``-d`` and ``--device`` as parameter name. * Controllers must be able to operate in "simulation" mode, where they behave properly even if the associated hardware is not connected. For example, they can print the data to the console instead of sending it to the device, or dump it into a file. -* We suggest that the simulation mode is entered whenever the ``-d/--device`` option is omitted. +* The simulation mode is entered whenever the ``--simulation`` option is specified. * Keep command line parameters consistent across clients/controllers. When adding new command line options, look for a client/controller that does a similar thing and follow its use of ``argparse``. If the original client/controller could use ``argparse`` in a better way, improve it. * Use docstrings for all public methods of the driver (note that those will be retrieved by ``artiq_rpctool``). * Choose a free default TCP port and add it to the default port list in this manual. diff --git a/doc/manual/faq.rst b/doc/manual/faq.rst index de9e59006..c30912291 100644 --- a/doc/manual/faq.rst +++ b/doc/manual/faq.rst @@ -7,30 +7,19 @@ How do I ... prevent my first RTIO command from causing an underflow? -------------------------------------------------------- -The RTIO timestamp counter starts counting at zero at the beginning of the first kernel run on the core device. The first RTIO event is programmed with a small timestamp above zero. If the kernel needs more time than this timestamp to produce the event, an underflow will occur. You can prevent it by calling ``break_realtime`` just before programming the first event. - -override the `sysclk` frequency of just one DDS? ------------------------------------------------- - -Override the parameter using an argument in the DDB. +The first RTIO event is programmed with a small timestamp above the value of the timecounter at the start of the experiment. If the kernel needs more time than this timestamp to produce the event, an underflow will occur. You can prevent it by calling ``break_realtime`` just before programming the first event, or by adding a sufficient delay. organize parameters in folders? ------------------------------- -Use GUI auto-completion and filtering. -Names need to be unique. +Folders are not supported yet, use GUI filtering for now. Names need to be unique. enforce functional dependencies between parameters? --------------------------------------------------- If you want to override a parameter ``b`` in the PDB to be ``b = 2*a``, use wrapper experiments, overriding parameters by passing them to the -experiment's constructor. - -get rid of ``DBKeys``? ----------------------- - -``DBKeys`` references keys in PDB, DDB and RDB. +experiment's constructor (``param_override`` argument). write a generator feeding a kernel feeding an analyze function? --------------------------------------------------------------- @@ -74,3 +63,24 @@ sub-experiments difficult and fragile. You can however always use the scheduler API to achieve the same (``scheduler.yield(duration=0)``) or wrap your own generators/coroutines/tasks in regular functions that you then expose as part of the API. + +determine the pyserial URL to attach to a device by its serial number? +---------------------------------------------------------------------- + +You can list your system's serial devices and print their vendor/product +id and serial number by running:: + + $ python3 -m serial.tools.list_ports -v + +It will give you the ``/dev/ttyUSBxx`` (or the ``COMxx`` for Windows) device +names. +The ``hwid:`` field gives you the string you can pass via the ``hwgrep://`` +feature of pyserial +`serial_for_url() `_ +in order to open a serial device. + +The preferred way to specify a serial device is to make use of the ``hwgrep://`` +URL: it allows to select the serial device by its USB vendor ID, product +ID and/or serial number. Those never change, unlike the device file name. + +See the :ref:`TDC001 documentation ` for an example of ``hwgrep://`` usage. diff --git a/doc/manual/fpga_board_ports.rst b/doc/manual/fpga_board_ports.rst index 13ba6b54a..52bd8dffe 100644 --- a/doc/manual/fpga_board_ports.rst +++ b/doc/manual/fpga_board_ports.rst @@ -13,20 +13,26 @@ The low-cost Pipistrello FPGA board can be used as a lower-cost but slower alter When plugged to an adapter, the NIST QC1 hardware can be used. The TTL lines are mapped to RTIO channels as follows: -+--------------+----------+-----------------+ -| RTIO channel | TTL line | Capability | -+==============+==========+=================+ -| 0 | PMT0 | Input only | -+--------------+----------+-----------------+ -| 1 | PMT1 | Input only | -+--------------+----------+-----------------+ -| 2-18 | TTL0-16 | Output only | -+--------------+----------+-----------------+ -| 19-21 | LEDs | Output only | -+--------------+----------+-----------------+ -| 22 | TTL2 | Output only | -+--------------+----------+-----------------+ ++--------------+----------+------------+ +| RTIO channel | TTL line | Capability | ++==============+==========+============+ +| 0 | PMT0 | Input | ++--------------+----------+------------+ +| 1 | PMT1 | Input | ++--------------+----------+------------+ +| 2-16 | TTL0-14 | Output | ++--------------+----------+------------+ +| 17 | TTL15 | Clock | ++--------------+----------+------------+ +| 18 | EXT_LED | Output | ++--------------+----------+------------+ +| 19 | USER_LED | Output | ++--------------+----------+------------+ +| 20 | DDS | Output | ++--------------+----------+------------+ The input only limitation on channels 0 and 1 comes from the QC-DAQ adapter. When the adapter is not used (and physically unplugged from the Pipistrello board), the corresponding pins on the Pipistrello can be used as outputs. Do not configure these channels as outputs when the adapter is plugged, as this would cause electrical contention. -The board can accept an external RTIO clock connected to PMT2. +The board can accept an external RTIO clock connected to PMT2. If the DDS box +does not drive the PMT2 pair, use XTRIG and patch the XTRIG transciever output +on the adapter board onto C:15 disconnecting PMT2. diff --git a/doc/manual/getting_started.rst b/doc/manual/getting_started.rst index eb3166f6c..5a5fd1329 100644 --- a/doc/manual/getting_started.rst +++ b/doc/manual/getting_started.rst @@ -1,6 +1,8 @@ Getting started =============== +.. _connecting-to-the-core-device: + Connecting to the core device ----------------------------- @@ -9,23 +11,23 @@ As a very first step, we will turn on a LED on the core device. Create a file `` from artiq import * - class LED(Experiment, AutoDB): - class DBKeys: - core = Device() - led = Device() + class LED(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("led") @kernel def run(self): self.led.on() -The central part of our code is our ``LED`` class, that derives from :class:`artiq.language.db.AutoDB`. ``AutoDB`` is part of the mechanism that attaches device drivers and retrieves parameters according to a database. Our ``DBKeys`` class lists the devices (and parameters) that ``LED`` needs in order to operate, and the names of the attributes (e.g. ``led``) are used to search the database. ``AutoDB`` replaces them with the actual device drivers (and parameter values). Finally, the ``@kernel`` decorator tells the system that the ``run`` method must be executed on the core device (instead of the host). +The central part of our code is our ``LED`` class, that derives from :class:`artiq.language.environment.EnvExperiment`. Among other features, ``EnvExperiment`` calls our ``build`` method and provides the ``attr_device`` method that interfaces to the device database to create the appropriate device drivers and make those drivers accessible as ``self.core`` and ``self.led``. The ``@kernel`` decorator tells the system that the ``run`` method must be executed on the core device (instead of the host). The decorator uses ``self.core`` internally, which is why we request the core device using ``attr_device`` like any other. Copy the files ``ddb.pyon`` and ``pdb.pyon`` (containing the device and parameter databases) from the ``examples`` folder of ARTIQ into the same directory as ``led.py`` (alternatively, you can use the ``-d`` and ``-p`` options of ``artiq_run.py``). You can open the database files using a text editor - their contents are in a human-readable format. -Run your code using ``artiq_run.py``, which is part of the ARTIQ front-end tools: :: +Run your code using ``artiq_run``, which is part of the ARTIQ front-end tools: :: - $ artiq_run.py led.py + $ artiq_run led.py The LED of the device should turn on. Congratulations! You have a basic ARTIQ system up and running. @@ -39,10 +41,10 @@ Modify the code as follows: :: def input_led_state(): return int(input("Enter desired LED state: ")) - class LED(Experiment, AutoDB): - class DBKeys: - core = Device() - led = Device() + class LED(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("led") @kernel def run(self): @@ -56,9 +58,9 @@ Modify the code as follows: :: You can then turn the LED off and on by entering 0 or 1 at the prompt that appears: :: - $ artiq_run.py led.py + $ artiq_run led.py Enter desired LED state: 1 - $ artiq_run.py led.py + $ artiq_run led.py Enter desired LED state: 0 What happens is the ARTIQ compiler notices that the ``input_led_state`` function does not have a ``@kernel`` decorator and thus must be executed on the host. When the core device calls it, it sends a request to the host to execute it. The host displays the prompt, collects user input, and sends the result back to the core device, which sets the LED state accordingly. @@ -88,10 +90,10 @@ Create a new file ``rtio.py`` containing the following: :: from artiq import * - class Tutorial(Experiment, AutoDB): - class DBKeys: - core = Device() - ttl0 = Device() + class Tutorial(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl0") @kernel def run(self): @@ -111,10 +113,10 @@ Try reducing the period of the generated waveform until the CPU cannot keep up w def print_underflow(): print("RTIO underflow occured") - class Tutorial(Experiment, AutoDB): - class DBKeys: - core = Device() - ttl0 = Device() + class Tutorial(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("ttl0") @kernel def run(self): diff --git a/doc/manual/index.rst b/doc/manual/index.rst index b17348981..502ea2443 100644 --- a/doc/manual/index.rst +++ b/doc/manual/index.rst @@ -15,6 +15,7 @@ Contents: core_drivers_reference protocols_reference ndsp_reference + core_device_flash_storage utilities fpga_board_ports default_network_ports diff --git a/doc/manual/installing.rst b/doc/manual/installing.rst index 7aa7c9ea4..dec7eab80 100644 --- a/doc/manual/installing.rst +++ b/doc/manual/installing.rst @@ -1,8 +1,79 @@ Installing ARTIQ ================ +The preferred way of installing ARTIQ is through the use of the conda package manager. +The conda package contains pre-built binaries that you can directly flash to your board. +But you can also :ref:`install from sources `. + +.. note:: Only the linux-64 and linux-32 conda packages contain the FPGA/BIOS/runtime pre-built binaries. + +Installing using conda +---------------------- + +Installing Anaconda or Miniconda +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +* You can either install Anaconda (chose Python 3.4) from https://store.continuum.io/cshop/anaconda/ + +* Or install the more minimalistic Miniconda (chose Python3.4) from http://conda.pydata.org/miniconda.html + +After installing either Anaconda or Miniconda, open a new terminal and make sure the following command works:: + + $ conda + +If it prints the help of the ``conda`` command, your install is OK. +If not, then make sure your ``$PATH`` environment variable contains the path to anaconda3/bin (or miniconda3/bin):: + + $ echo $PATH + /home/fallen/miniconda3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games + +If your ``$PATH`` misses reference the miniconda3/bin or anaconda3/bin you can fix this by typing:: + + $ export PATH=$HOME/miniconda3:$PATH + +Installing the host side software +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +For this, you need to add our binstar repository to your conda configuration:: + + $ conda config --add channels http://conda.anaconda.org/fallen/channel/dev + +Then you can install the ARTIQ package, it will pull all the necessary dependencies:: + + $ conda install artiq + Preparing the core device FPGA board ------------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You now need to flash 3 things on the FPGA board: + +1. The FPGA bitstream +2. The BIOS +3. The ARTIQ runtime + +First you need to :ref:`install xc3sprog `. Then, you can flash the board: + +* For the Pipistrello board:: + + $ artiq_flash.sh -t pipistrello + +* For the KC705 board:: + + $ artiq_flash.sh + +Next step (for KC705) is to flash MAC and IP addresses to the board: + +* See :ref:`those instructions ` to flash MAC and IP addresses. + +.. _install-from-sources: + +Installing from source +---------------------- + +You can skip this if you already installed from conda. + +Preparing the core device FPGA board +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ These steps are required to generate bitstream (``.bit``) files, build the MiSoC BIOS and ARTIQ runtime, and flash FPGA boards. If the board is already flashed, you may skip those steps and go directly to `Installing the host-side software`. @@ -22,7 +93,7 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC $ cd ~/artiq-dev $ git clone https://github.com/m-labs/migen - $ cd ~/artiq-dev/migen + $ cd migen $ python3 setup.py develop --user .. note:: @@ -30,11 +101,11 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC * Install OpenRISC GCC/binutils toolchain (or1k-elf-...): :: - $ mkdir ~/artiq-dev $ cd ~/artiq-dev $ git clone https://github.com/openrisc/or1k-src - $ mkdir ~/artiq-dev/or1k-src/build - $ cd ~/artiq-dev/or1k-src/build + $ cd or1k-src + $ mkdir build + $ cd build $ ../configure --target=or1k-elf --enable-shared --disable-itcl \ --disable-tk --disable-tcl --disable-winsup \ --disable-gdbtk --disable-libgui --disable-rda \ @@ -45,26 +116,31 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC $ cd ~/artiq-dev $ git clone https://github.com/openrisc/or1k-gcc - $ mkdir ~/artiq-dev/or1k-gcc/build - $ cd ~/artiq-dev/or1k-gcc/build + $ cd or1k-gcc + $ mkdir build + $ cd build $ ../configure --target=or1k-elf --enable-languages=c \ --disable-shared --disable-libssp $ make -j4 $ sudo make install +.. _install-xc3sprog: + * Install JTAG tools needed to program the Pipistrello and KC705: :: $ cd ~/artiq-dev $ svn co https://xc3sprog.svn.sourceforge.net/svnroot/xc3sprog/trunk xc3sprog - $ cd ~/artiq-dev/xc3sprog + $ cd xc3sprog $ cmake . && make $ sudo make install .. note:: It is safe to ignore the message "Could NOT find LIBFTD2XX" (libftd2xx is different from libftdi, and is not required). +.. _install-flash-proxy: + * Install the required flash proxy bitstreams: The purpose of the flash proxy bitstream is to give programming software fast JTAG access to the flash connected to the FPGA. @@ -84,8 +160,10 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC $ cd ~/artiq-dev $ git clone https://github.com/m-labs/bscan_spi_kc705 + $ cd bscan_spi_kc705 + $ make - Build the bitstream and copy it to one of the folders above. + Then copy the generated ``bscan_spi_kc705.bit`` to ``~/.migen``, ``/usr/local/share/migen`` or ``/usr/share/migen``. * Download MiSoC: :: @@ -99,10 +177,18 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC $ git clone https://github.com/m-labs/artiq $ python3 setup.py develop --user -* Build and flash the bitstream and BIOS by running `from the MiSoC top-level directory`: :: +* Build and flash the bitstream and BIOS by running `from the MiSoC top-level directory`: + :: $ cd ~/artiq-dev/misoc - $ ./make.py -X ~/artiq-dev/artiq/soc -t artiq_ppro all + + * For Pipistrello:: + + $ ./make.py -X ~/artiq-dev/artiq/soc -t artiq_pipistrello all + + * For KC705:: + + $ ./make.py -X ~/artiq-dev/artiq/soc -t artiq_kc705 all * Then, build and flash the ARTIQ runtime: :: @@ -124,27 +210,95 @@ These steps are required to generate bitstream (``.bit``) files, build the MiSoC The communication parameters are 115200 8-N-1. +.. _flash-mac-ip-addr: + +* Set the MAC and IP address in the :ref:`core device configuration flash storage `: + + * You can either set it by generating a flash storage image and then flash it: :: + + $ artiq_mkfs flash_storage.img -s mac xx:xx:xx:xx:xx:xx -s ip xx.xx.xx.xx + $ ~/artiq-dev/artiq/frontend/artiq_flash.sh -f flash_storage.img + + * Or you can set it via the runtime test mode command line + + * Boot the board. + + * Quickly run flterm (in ``path/to/misoc/tools``) to access the serial console. + + * If you weren't quick enough to see anything in the serial console, press the reset button. + + * Wait for "Press 't' to enter test mode..." to appear and hit the ``t`` key. + + * Enter the following commands (which will erase the flash storage content). + + :: + + test> fserase + test> fswrite ip xx.xx.xx.xx + test> fswrite mac xx:xx:xx:xx:xx:xx + + * Then reboot. + + You should see something like this in the serial console: :: + + ~/dev/misoc$ ./tools/flterm --port /dev/ttyUSB1 + [FLTERM] Starting... + + MiSoC BIOS http://m-labs.hk + (c) Copyright 2007-2014 Sebastien Bourdeauducq + [...] + Press 't' to enter test mode... + Entering test mode. + test> fserase + test> fswrite ip 192.168.10.2 + test> fswrite mac 11:22:33:44:55:66 + +.. note:: The reset button of the KC705 board is the "CPU_RST" labeled button. +.. warning:: Both those instructions will result in the flash storage being wiped out. However you can use the test mode to change the IP/MAC without erasing everything if you skip the "fserase" command. + +* (optional) Flash the ``idle`` kernel + +The ``idle`` kernel is the kernel (some piece of code running on the core device) which the core device runs whenever it is not connected to a PC via ethernet. +This kernel is therefore stored in the :ref:`core device configuration flash storage `. +To flash the ``idle`` kernel: + + * Compile the ``idle`` experiment: + The ``idle`` experiment's ``run()`` method must be a kernel: it must be decorated with the ``@kernel`` decorator (see :ref:`next topic ` for more information about kernels). + + Since the core device is not connected to the PC, RPCs (calling Python code running on the PC from the kernel) are forbidden in the ``idle`` experiment. + :: + + $ artiq_compile idle.py + + * Write it into the core device configuration flash storage: :: + + $ artiq_coreconfig write -f idle_kernel idle.elf + +.. note:: You can find more information about how to use the ``artiq_coreconfig`` tool on the :ref:`Utilities ` page. + Installing the host-side software ---------------------------------- +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ * Install LLVM and the llvmlite Python bindings: :: $ cd ~/artiq-dev $ git clone https://github.com/openrisc/llvm-or1k - $ cd ~/artiq-dev/llvm-or1k/tools + $ cd llvm-or1k/tools $ git clone https://github.com/openrisc/clang-or1k clang - $ cd ~/artiq-dev/llvm-or1k + $ cd .. $ mkdir build - $ cd ~/artiq-dev/llvm-or1k/build - $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/llvm-or1k -DLLVM_TARGETS_TO_BUILD=OR1K -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON + $ cd build + $ cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local/llvm-or1k -DLLVM_TARGETS_TO_BUILD="OR1K;X86" -DCMAKE_BUILD_TYPE=Debug $ make -j4 $ sudo make install $ cd ~/artiq-dev $ git clone https://github.com/numba/llvmlite - $ cd ~/artiq-dev/llvmlite + $ cd llvmlite $ patch -p1 < ~/artiq-dev/artiq/misc/llvmlite-add-all-targets.patch + $ patch -p1 < ~/artiq-dev/artiq/misc/llvmlite-rename.patch + $ patch -p1 < ~/artiq-dev/artiq/misc/llvmlite-build-as-debug-on-windows.patch $ PATH=/usr/local/llvm-or1k/bin:$PATH sudo -E python3 setup.py install .. note:: @@ -153,31 +307,13 @@ Installing the host-side software .. note:: Compilation of LLVM can take more than 30 min on some machines. -* Install ARTIQ (without the GUI): :: +* Install ARTIQ: :: $ cd ~/artiq-dev $ git clone https://github.com/m-labs/artiq # if not already done $ cd artiq $ python3 setup.py develop --user -* Install ARTIQ (with the GUI): :: - - $ cd ~/artiq-dev - $ git clone https://github.com/m-labs/cairoplot3 - $ cd cairoplot3 - $ python3 setup.py install --user - $ cd - - $ git clone https://github.com/m-labs/gbulb - $ cd gbulb - $ python3 setup.py install --user - $ cd - - $ git clone https://github.com/m-labs/artiq # if not already done - $ cd artiq - $ ARTIQ_GUI=1 python3 setup.py develop --user - -.. note:: - Use ARTIQ_GUI=1 to install GUI dependencies which are only supported on Linux for now, to install ARTIQ on Windows do not set ARTIQ_GUI. - * Build the documentation: :: $ cd ~/artiq-dev/artiq/doc/manual @@ -188,7 +324,7 @@ Ubuntu 14.04 specific instructions This command installs all the required packages: :: - $ sudo apt-get install build-essential autotools-dev file git patch perl xutils-devs python3-pip texinfo flex bison libmpc-dev python3-serial python3-dateutil python3-prettytable python3-setuptools python3-numpy python3-scipy python3-sphinx python3-h5py python3-gi python3-dev python-dev subversion cmake libusb-dev libftdi-dev pkg-config + $ sudo apt-get install build-essential autotools-dev file git patch perl xutils-devs python3-pip texinfo flex bison libmpc-dev python3-serial python3-dateutil python3-prettytable python3-setuptools python3-numpy python3-scipy python3-sphinx python3-h5py python3-dev python-dev subversion cmake libusb-dev libftdi-dev pkg-config Note that ARTIQ requires Python 3.4 or above. diff --git a/doc/manual/ndsp_reference.rst b/doc/manual/ndsp_reference.rst index d7e454126..fa2f85e31 100644 --- a/doc/manual/ndsp_reference.rst +++ b/doc/manual/ndsp_reference.rst @@ -34,6 +34,28 @@ Client Lab Brick Digital Attenuator (LDA) ---------------------------------- +On Linux, you need to give your user access to the USB device. + +You can do that by creating a file under ``/etc/udev/rules.d/`` named +``99-lda.rules`` with the following content:: + + SUBSYSTEM=="usb", ATTR{idVendor}=="041f", MODE="0666" + +Then you need to tell udev to reload its rules:: + + $ sudo invoke-rc.d udev reload + +You must also unplug/replug your device if it was already plugged in. + +Then, to run the Lab Brick Digital Attenuator (LDA) controller:: + + $ lda_controller -d SN:xxxxx + +The serial number must contain exactly 5 digits, prepend it with the necessary number of 0s. +Also, the ``SN:`` prefix is mandatory. + +You can choose the LDA model with the ``-P`` parameter. The default is LDA-102. + Driver ++++++ @@ -66,6 +88,8 @@ Controller Thorlabs T-Cube --------------- +.. _tdc001-controller-usage-example: + TDC001 controller usage example +++++++++++++++++++++++++++++++ @@ -76,6 +100,19 @@ First, run the TDC001 controller:: .. note:: On Windows the serial port (the ``-d`` argument) will be of the form ``COMx``. +.. note:: + Anything compatible with `serial_for_url `_ + can be given as a device in ``-d`` argument. + + For instance, if you want to specify the Vendor/Product ID and the USB Serial Number, you can do: + + ``-d "hwgrep://: SNR="``. + for instance: + + ``-d "hwgrep://0403:faf0 SNR=83852734"`` + + The hwgrep URL works on both Linux and Windows. + Then, send commands to it via the ``artiq_rpctool`` utility:: $ artiq_rpctool ::1 3255 list-targets @@ -96,6 +133,11 @@ First, run the TPZ001 controller:: .. note:: On Windows the serial port (the ``-d`` argument) will be of the form ``COMx``. +.. note:: + See the :ref:`TDC001 documentation ` for + how to specify the USB Serial Number of the device instead of the + /dev/ttyUSBx (or the COMx name). + Then, send commands to it via the ``artiq_rpctool`` utility:: $ artiq_rpctool ::1 3255 list-targets diff --git a/doc/manual/protocols_reference.rst b/doc/manual/protocols_reference.rst index 070176acd..7ddfbc2ee 100644 --- a/doc/manual/protocols_reference.rst +++ b/doc/manual/protocols_reference.rst @@ -21,6 +21,14 @@ Protocols reference .. automodule:: artiq.protocols.pc_rpc :members: + +:mod:`artiq.protocols.fire_and_forget` module +--------------------------------------------- + +.. automodule:: artiq.protocols.fire_and_forget + :members: + + :mod:`artiq.protocols.sync_struct` module ----------------------------------------- diff --git a/doc/manual/utilities.rst b/doc/manual/utilities.rst index ebf6c2bb9..a507291ae 100644 --- a/doc/manual/utilities.rst +++ b/doc/manual/utilities.rst @@ -92,3 +92,61 @@ This tool compiles key/value pairs into a binary image suitable for flashing int .. argparse:: :ref: artiq.frontend.artiq_mkfs.get_argparser :prog: artiq_mkfs + +.. _core-device-configuration-tool: + +Core device configuration tool +------------------------------ + +The artiq_coreconfig tool allows to read, write and remove key-value records from the :ref:`core-device-flash-storage`. + +It also allows to erase the entire flash storage area. + +To use this tool, you need to specify a ``ddb.pyon`` DDB file which contains a ``comm`` device (an example is provided in ``artiq/examples/master/ddb.pyon``). +This tells the tool how to connect to the core device (via serial or via TCP) and with which parameters (baudrate, serial device, IP address, TCP port). +When not specified, the artiq_coreconfig tool will assume that there is a file named ``ddb.pyon`` in the current directory. + + +To read the record whose key is ``mac``:: + + $ artiq_coreconfig read mac + +To write the value ``test_value`` in the key ``my_key``:: + + $ artiq_coreconfig write -s my_key test_value + $ artiq_coreconfig read my_key + b'test_value' + +You can also write entire files in a record using the ``-f`` parameter. This is useful for instance to write the ``idle`` kernel in the flash storage:: + + $ artiq_coreconfig write -f idle_kernel idle.elf + $ artiq_coreconfig read idle_kernel | head -c9 + b'\x7fELF + +You can write several records at once:: + + $ artiq_coreconfig write -s key1 value1 -f key2 filename -s key3 value3 + +To remove the previously written key ``my_key``:: + + $ artiq_coreconfig delete my_key + +You can remove several keys at once:: + + $ artiq_coreconfig delete key1 key2 + +To erase the entire flash storage area:: + + $ artiq_coreconfig erase + +You don't need to remove a record in order to change its value, just overwrite +it:: + + $ artiq_coreconfig write -s my_key some_value + $ artiq_coreconfig write -s my_key some_other_value + $ artiq_coreconfig read my_key + b'some_other_value' + +.. argparse:: + :ref: artiq.frontend.artiq_coreconfig.get_argparser + :prog: artiq_coreconfig diff --git a/doc/slides/artiq_overview.tex b/doc/slides/artiq_overview.tex index 97605fa89..8f7ed66c2 100644 --- a/doc/slides/artiq_overview.tex +++ b/doc/slides/artiq_overview.tex @@ -91,18 +91,19 @@ inner sep=.3mm] at (current page.south east) {% \begin{minted}[frame=leftline]{python} trigger.sync() # wait for trigger input -start = now() # capture trigger time +start = now_mu() # capture trigger time for i in range(3): delay(5*us) dds.pulse(900*MHz, 7*us) # first pulse 5 µs after trigger -at(start + 1*ms) # re-reference time-line +# re-reference time-line +at(start + seconds_to_mu(1*ms)) dds.pulse(200*MHz, 11*us) # exactly 1 ms after trigger \end{minted} \begin{itemize} \item Written in a subset of Python \item Executed on a CPU embedded on a FPGA (the \emph{core device}) - \item \verb!now(), at(), delay()! describe time-line of an experiment + \item \verb!now_mu(), at_mu(), delay_mu(), delay()! describe time-line of an experiment \item Exact time is kept in an internal variable \item That variable only loosely tracks the execution time of CPU instructions \item The value of that variable is exchanged with the RTIO fabric that @@ -148,11 +149,12 @@ dds.on(f, phase=0) # must round to integer tuning word for i in range(n): delay(dt) # must round to native cycles -dt_raw = time_to_cycles(dt) # integer number of cycles +dt_raw = seconds_to_mu(dt) # integer number of cycles f_raw = dds.frequency_to_ftw(f) # integer frequency tuning word -# determine correct phase despite accumulation of rounding errors -phi = n*cycles_to_time(dt_raw)*dds.ftw_to_frequency(f_raw) +# determine correct (to FP precision) phase +# despite accumulation of rounding errors +phi = mu_to_seconds(n*dt_raw)*dds.ftw_to_frequency(f_raw) \end{minted} \begin{itemize} diff --git a/doc/slides/taaccs.tex b/doc/slides/taaccs.tex index 09c263307..71c608f26 100644 --- a/doc/slides/taaccs.tex +++ b/doc/slides/taaccs.tex @@ -105,18 +105,19 @@ inner sep=.3mm] at (current page.south east) {% \begin{minted}[frame=leftline]{python} trigger.sync() # wait for trigger input -start = now() # capture trigger time +start = now_mu() # capture trigger time for i in range(3): delay(5*us) dds.pulse(900*MHz, 7*us) # first pulse 5 µs after trigger -at(start + 1*ms) # re-reference time-line +# re-reference time-line +at_mu(start + seconds_to_mu(1*ms)) dds.pulse(200*MHz, 11*us) # exactly 1 ms after trigger \end{minted} \begin{itemize} \item Written in a subset of Python \item Executed on a CPU embedded on a FPGA (the \emph{core device}) - \item \verb!now(), at(), delay()! describe time-line of an experiment + \item \verb!now_mu(), at_mu(), delay_mu(), delay()! describe time-line of an experiment \item Exact time is kept in an internal variable \item That variable only loosely tracks the execution time of CPU instructions \item The value of that variable is exchanged with the RTIO fabric that @@ -162,11 +163,12 @@ dds.on(f, phase=0) # must round to integer tuning word for i in range(n): delay(dt) # must round to native cycles -dt_raw = time_to_cycles(dt) # integer number of cycles +dt_raw = seconds_to_mu(dt) # integer number of cycles f_raw = dds.frequency_to_ftw(f) # integer frequency tuning word -# determine correct phase despite accumulation of rounding errors -phi = n*cycles_to_time(dt_raw)*dds.ftw_to_frequency(f_raw) +# determine correct (to FP precision) phase +# despite accumulation of rounding errors +phi = n*mu_to_seconds(dt_raw)*dds.ftw_to_frequency(f_raw) \end{minted} \begin{itemize} diff --git a/examples/master/ddb.pyon b/examples/master/ddb.pyon index 7e7778f66..ff945141a 100644 --- a/examples/master/ddb.pyon +++ b/examples/master/ddb.pyon @@ -43,6 +43,12 @@ "class": "TTLOut", "arguments": {"channel": 4} }, + "ttl3": { + "type": "local", + "module": "artiq.coredevice.ttl", + "class": "TTLOut", + "arguments": {"channel": 5} + }, "led": { "type": "local", "module": "artiq.coredevice.ttl", @@ -59,20 +65,20 @@ "dds0": { "type": "local", "module": "artiq.coredevice.dds", - "class": "DDS", - "arguments": {"channel": 0} + "class": "AD9858", + "arguments": {"sysclk": 1e9, "channel": 0} }, "dds1": { "type": "local", "module": "artiq.coredevice.dds", - "class": "DDS", - "arguments": {"channel": 1} + "class": "AD9858", + "arguments": {"sysclk": 1e9, "channel": 1} }, "dds2": { "type": "local", "module": "artiq.coredevice.dds", - "class": "DDS", - "arguments": {"channel": 2} + "class": "AD9858", + "arguments": {"sysclk": 1e9, "channel": 2} }, "qc_q1_0": { @@ -113,10 +119,9 @@ "class": "CompoundPDQ2", "arguments": { "pdq2_devices": ["qc_q1_0", "qc_q1_1", "qc_q1_2", "qc_q1_3"], - "rtio_trigger": 7, - "rtio_frame": (2, 3, 4) - }, - "comment": "Conflicts with dds2 and ttl0-2" + "trigger_device": "ttl3", + "frame_devices": ["ttl0", "ttl1", "ttl2"] + } }, "lda": { diff --git a/examples/master/repository/arguments_demo.py b/examples/master/repository/arguments_demo.py new file mode 100644 index 000000000..97393df19 --- /dev/null +++ b/examples/master/repository/arguments_demo.py @@ -0,0 +1,21 @@ +from artiq import * + + +class ArgumentsDemo(EnvExperiment): + def build(self): + self.attr_argument("free_value", FreeValue(None)) + self.attr_argument("boolean", BooleanValue(True)) + self.attr_argument("enum", EnumerationValue( + ["foo", "bar", "quux"], "foo")) + self.attr_argument("number", NumberValue(42, unit="s", step=0.1)) + self.attr_argument("string", StringValue("Hello World")) + self.attr_argument("scan", Scannable(global_max=400, default=NoScan(325))) + + def run(self): + print(self.free_value) + print(self.boolean) + print(self.enum) + print(self.number) + print(self.string) + for i in self.scan: + print(i) diff --git a/examples/master/repository/dds_test.py b/examples/master/repository/dds_test.py index a36af8447..ff4458abc 100644 --- a/examples/master/repository/dds_test.py +++ b/examples/master/repository/dds_test.py @@ -1,19 +1,19 @@ from artiq import * -class DDSTest(Experiment, AutoDB): +class DDSTest(EnvExperiment): """DDS test""" - class DBKeys: - core = Device() - dds_bus = Device() - dds0 = Device() - dds1 = Device() - dds2 = Device() - ttl0 = Device() - ttl1 = Device() - ttl2 = Device() - led = Device() + def build(self): + self.attr_device("core") + self.attr_device("dds_bus") + self.attr_device("dds0") + self.attr_device("dds1") + self.attr_device("dds2") + self.attr_device("ttl0") + self.attr_device("ttl1") + self.attr_device("ttl2") + self.attr_device("led") @kernel def run(self): diff --git a/examples/master/repository/flopping_f_simulation.py b/examples/master/repository/flopping_f_simulation.py index 9aa2bc9d8..19a5127d4 100644 --- a/examples/master/repository/flopping_f_simulation.py +++ b/examples/master/repository/flopping_f_simulation.py @@ -23,25 +23,21 @@ def model_numpy(xdata, F0): return r -class FloppingF(Experiment, AutoDB): +class FloppingF(EnvExperiment): """Flopping F simulation""" - class DBKeys: - npoints = Argument(100) - min_freq = Argument(1000) - max_freq = Argument(2000) + def build(self): + self.attr_argument("npoints", FreeValue(100)) + self.attr_argument("min_freq", FreeValue(1000)) + self.attr_argument("max_freq", FreeValue(2000)) - F0 = Argument(1500) - noise_amplitude = Argument(0.1) + self.attr_argument("F0", FreeValue(1500)) + self.attr_argument("noise_amplitude", FreeValue(0.1)) - frequency = Result() - brightness = Result() + self.frequency = self.set_result("flopping_f_frequency", [], True) + self.brightness = self.set_result("flopping_f_brightness", [], True) - flopping_freq = Parameter() - - realtime_results = { - ("frequency", "brightness"): "xy" - } + self.attr_device("scheduler") def run(self): for i in range(self.npoints): @@ -51,12 +47,12 @@ class FloppingF(Experiment, AutoDB): self.brightness.append(brightness) time.sleep(0.1) self.scheduler.submit(self.scheduler.pipeline_name, self.scheduler.expid, - self.scheduler.priority, time.time() + 20) + self.scheduler.priority, time.time() + 20, False) def analyze(self): popt, pcov = curve_fit(model_numpy, self.frequency.read, self.brightness.read, - p0=[self.flopping_freq]) + p0=[self.get_parameter("flopping_freq")]) perr = np.sqrt(np.diag(pcov)) if perr < 0.1: - self.flopping_freq = float(popt) + self.set_parameter("flopping_freq", float(popt)) diff --git a/examples/master/repository/handover.py b/examples/master/repository/handover.py index 8974678a9..5870d27ba 100644 --- a/examples/master/repository/handover.py +++ b/examples/master/repository/handover.py @@ -1,10 +1,10 @@ from artiq import * -class Handover(Experiment, AutoDB): - class DBKeys: - core = Device() - led = Device() +class Handover(EnvExperiment): + def build(self): + self.attr_device("core") + self.attr_device("led") @kernel def blink_once(self): diff --git a/examples/master/repository/mandelbrot.py b/examples/master/repository/mandelbrot.py index 0a0c030a6..a14e58900 100644 --- a/examples/master/repository/mandelbrot.py +++ b/examples/master/repository/mandelbrot.py @@ -3,11 +3,11 @@ import sys from artiq import * -class Mandelbrot(Experiment, AutoDB): +class Mandelbrot(EnvExperiment): """Mandelbrot set demo""" - class DBKeys: - core = Device() + def build(self): + self.attr_device("core") def col(self, i): sys.stdout.write(" .,-:;i+hHM$*#@ "[i]) diff --git a/examples/master/repository/photon_histogram.py b/examples/master/repository/photon_histogram.py index aebe1beb4..bad44e1b7 100644 --- a/examples/master/repository/photon_histogram.py +++ b/examples/master/repository/photon_histogram.py @@ -1,29 +1,24 @@ from artiq import * -class PhotonHistogram(Experiment, AutoDB): +class PhotonHistogram(EnvExperiment): """Photon histogram""" - class DBKeys: - core = Device() - dds_bus = Device() - bd_dds = Device() - bd_sw = Device() - bdd_dds = Device() - bdd_sw = Device() - pmt = Device() + def build(self): + self.attr_device("core") + self.attr_device("dds_bus") + self.attr_device("bd_dds") + self.attr_device("bd_sw") + self.attr_device("bdd_dds") + self.attr_device("bdd_sw") + self.attr_device("pmt") - nbins = Argument(100) - repeats = Argument(100) + self.attr_argument("nbins", FreeValue(100)) + self.attr_argument("repeats", FreeValue(100)) - cool_f = Parameter(230*MHz) - detect_f = Parameter(220*MHz) - detect_t = Parameter(100*us) - - ion_present = Parameter(True) - - hist = Result() - total = Result() + self.attr_parameter("cool_f", 230*MHz) + self.attr_parameter("detect_f", 220*MHz) + self.attr_parameter("detect_t", 100*us) @kernel def program_cooling(self): @@ -65,9 +60,8 @@ class PhotonHistogram(Experiment, AutoDB): hist[n] += 1 total += n - self.hist = hist - self.total = total - self.ion_present = total > 5*self.repeats + self.set_result("cooling_photon_histogram", hist) + self.set_parameter("ion_present", total > 5*self.repeats) if __name__ == "__main__": diff --git a/examples/master/repository/transport.py b/examples/master/repository/transport.py index ce1b2ece4..ed44e0a26 100644 --- a/examples/master/repository/transport.py +++ b/examples/master/repository/transport.py @@ -10,23 +10,22 @@ transport_data = dict( # 4 devices, 3 board each, 3 dacs each ) -class Transport(Experiment, AutoDB): +class Transport(EnvExperiment): """Transport""" - class DBKeys: - core = Device() - bd = Device() - bdd = Device() - pmt = Device() - electrodes = Device() + def build(self): + self.attr_device("core") + self.attr_device("bd") + self.attr_device("bdd") + self.attr_device("pmt") + self.attr_device("electrodes") - wait_at_stop = Parameter(100*us) - speed = Parameter(1.5) + self.attr_argument("wait_at_stop", FreeValue(100*us)) + self.attr_argument("speed", FreeValue(1.5)) + self.attr_argument("repeats", FreeValue(100)) + self.attr_argument("nbins", FreeValue(100)) - repeats = Argument(100) - nbins = Argument(100) - - def prepare(self, stop): + def calc_waveforms(self, stop): t = transport_data["t"][:stop]*self.speed u = transport_data["u"][:stop] @@ -89,9 +88,9 @@ class Transport(Experiment, AutoDB): def scan(self, stops): for s in stops: self.histogram = [] - # non-kernel, calculate waveforms, build frames + # non-kernel, build frames # could also be rpc'ed from repeat() - self.prepare(s) + self.calc_waveforms(s) # kernel part self.repeat() # live update 2d plot with current self.histogram diff --git a/examples/sim/al_spectroscopy.py b/examples/sim/al_spectroscopy.py index f41f09886..7f52fc32c 100644 --- a/examples/sim/al_spectroscopy.py +++ b/examples/sim/al_spectroscopy.py @@ -1,20 +1,20 @@ from artiq import * -class AluminumSpectroscopy(Experiment, AutoDB): +class AluminumSpectroscopy(EnvExperiment): """Aluminum spectroscopy (simulation)""" - class DBKeys: - core = Device() - mains_sync = Device() - laser_cooling = Device() - spectroscopy = Device() - spectroscopy_b = Device() - state_detection = Device() - pmt = Device() - spectroscopy_freq = Parameter(432*MHz) - photon_limit_low = Argument(10) - photon_limit_high = Argument(15) + def build(self): + self.attr_device("core") + self.attr_device("mains_sync") + self.attr_device("laser_cooling") + self.attr_device("spectroscopy") + self.attr_device("spectroscopy_b") + self.attr_device("state_detection") + self.attr_device("pmt") + self.attr_parameter("spectroscopy_freq", 432*MHz) + self.attr_argument("photon_limit_low", FreeValue(10)) + self.attr_argument("photon_limit_high", FreeValue(15)) @kernel def run(self): diff --git a/examples/sim/simple_simulation.py b/examples/sim/simple_simulation.py index e0e9d2b0c..602fcbd54 100644 --- a/examples/sim/simple_simulation.py +++ b/examples/sim/simple_simulation.py @@ -1,15 +1,13 @@ from artiq import * -class SimpleSimulation(Experiment, AutoDB): +class SimpleSimulation(EnvExperiment): """Simple simulation""" - class DBKeys: - core = Device() - a = Device() - b = Device() - c = Device() - d = Device() + def build(self): + self.attr_device("core") + for wo in "abcd": + self.attr_device(wo) @kernel def run(self): @@ -23,16 +21,13 @@ class SimpleSimulation(Experiment, AutoDB): def main(): - from artiq.sim import devices as sd + from artiq.sim import devices - core = sd.Core() - exp = SimpleSimulation( - core=core, - a=sd.WaveOutput(core=core, name="a"), - b=sd.WaveOutput(core=core, name="b"), - c=sd.WaveOutput(core=core, name="c"), - d=sd.WaveOutput(core=core, name="d"), - ) + dmgr = dict() + dmgr["core"] = devices.Core(dmgr) + for wo in "abcd": + dmgr[wo] = devices.WaveOutput(dmgr, wo) + exp = SimpleSimulation(dmgr) exp.run() if __name__ == "__main__": diff --git a/misc/llvmlite-build-as-debug-on-windows.patch b/misc/llvmlite-build-as-debug-on-windows.patch new file mode 100644 index 000000000..e385fb4a2 --- /dev/null +++ b/misc/llvmlite-build-as-debug-on-windows.patch @@ -0,0 +1,13 @@ +diff --git a/ffi/build.py b/ffi/build.py +index 3889ba5..58f93ec 100755 +--- a/ffi/build.py ++++ b/ffi/build.py +@@ -58,7 +58,7 @@ def find_win32_generator(): + + def main_win32(): + generator = find_win32_generator() +- config = 'Release' ++ config = 'Debug' + if not os.path.exists(build_dir): + os.mkdir(build_dir) + try_cmake(here_dir, build_dir, generator) diff --git a/misc/llvmlite-rename.patch b/misc/llvmlite-rename.patch new file mode 100644 index 000000000..faea85104 --- /dev/null +++ b/misc/llvmlite-rename.patch @@ -0,0 +1,65 @@ +diff --git a/setup.py b/setup.py +index 6d28265..f4edd29 100644 +--- a/setup.py ++++ b/setup.py +@@ -15,10 +15,10 @@ from llvmlite.utils import get_library_files + import versioneer + + versioneer.VCS = 'git' +-versioneer.versionfile_source = 'llvmlite/_version.py' +-versioneer.versionfile_build = 'llvmlite/_version.py' ++versioneer.versionfile_source = 'llvmlite_or1k/_version.py' ++versioneer.versionfile_build = 'llvmlite_or1k/_version.py' + versioneer.tag_prefix = 'v' # tags are like v1.2.0 +-versioneer.parentdir_prefix = 'llvmlite-' # dirname like 'myproject-1.2.0' ++versioneer.parentdir_prefix = 'llvmlite_or1k-' # dirname like 'myproject-1.2.0' + + + here_dir = os.path.dirname(__file__) +@@ -54,7 +54,7 @@ class LlvmliteBuildExt(build_ext): + # HACK: this makes sure the library file (which is large) is only + # included in binary builds, not source builds. + self.distribution.package_data = { +- "llvmlite.binding": get_library_files(), ++ "llvmlite_or1k.binding": get_library_files(), + } + + +@@ -63,7 +63,7 @@ class LlvmliteInstall(install): + # This seems to only be necessary on OSX. + def run(self): + self.distribution.package_data = { +- "llvmlite.binding": get_library_files(), ++ "llvmlite_or1k.binding": get_library_files(), + } + install.run(self) + +@@ -74,14 +74,14 @@ cmdclass.update({'build': LlvmliteBuild, + }) + + +-packages = ['llvmlite', +- 'llvmlite.binding', +- 'llvmlite.ir', +- 'llvmlite.llvmpy', +- 'llvmlite.tests', ++packages = ['llvmlite_or1k', ++ 'llvmlite_or1k.binding', ++ 'llvmlite_or1k.ir', ++ 'llvmlite_or1k.llvmpy', ++ 'llvmlite_or1k.tests', + ] + +-setup(name='llvmlite', ++setup(name='llvmlite_or1k', + description="lightweight wrapper around basic LLVM functionality", + version=versioneer.get_version(), + classifiers=[ +@@ -96,6 +96,7 @@ setup(name='llvmlite', + "Topic :: Software Development :: Code Generators", + "Topic :: Software Development :: Compilers", + ], ++ package_dir={"llvmlite_or1k" : "llvmlite"}, + # Include the separately-compiled shared library + author="Continuum Analytics, Inc.", + author_email="numba-users@continuum.io", diff --git a/misc/pyqtgraph-do-not-close-nonclosable-docks.patch b/misc/pyqtgraph-do-not-close-nonclosable-docks.patch new file mode 100644 index 000000000..307b664bc --- /dev/null +++ b/misc/pyqtgraph-do-not-close-nonclosable-docks.patch @@ -0,0 +1,28 @@ +diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py +index 4493d07..a05e685 100644 +--- a/pyqtgraph/dockarea/Dock.py ++++ b/pyqtgraph/dockarea/Dock.py +@@ -18,6 +18,7 @@ class Dock(QtGui.QWidget, DockDrop): + self.label = DockLabel(name, self, closable) + if closable: + self.label.sigCloseClicked.connect(self.close) ++ self.closable = closable + self.labelHidden = False + self.moveLabel = True ## If false, the dock is no longer allowed to move the label. + self.autoOrient = autoOrientation +diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py +index ffe75b6..b054b24 100644 +--- a/pyqtgraph/dockarea/DockArea.py ++++ b/pyqtgraph/dockarea/DockArea.py +@@ -306,7 +306,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): + def clear(self): + docks = self.findAll()[1] + for dock in docks.values(): +- dock.close() ++ if dock.closable: ++ dock.close() ++ else: ++ self.home.moveDock(dock, "top", None) + + ## PySide bug: We need to explicitly redefine these methods + ## or else drag/drop events will not be delivered. diff --git a/setup.py b/setup.py index 427b9e9aa..1a04c1309 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,11 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages +import sys +import os +if sys.version_info[:3] < (3, 4, 3): + raise Exception("You need at least Python 3.4.3 to run ARTIQ") requirements = [ "sphinx", "sphinx-argparse", "pyserial", "numpy", "scipy", @@ -12,6 +16,7 @@ requirements = [ scripts = [ "artiq_client=artiq.frontend.artiq_client:main", "artiq_compile=artiq.frontend.artiq_compile:main", + "artiq_coreconfig=artiq.frontend.artiq_coreconfig:main", "artiq_ctlmgr=artiq.frontend.artiq_ctlmgr:main", "artiq_gui=artiq.frontend.artiq_gui:main", "artiq_master=artiq.frontend.artiq_master:main", @@ -37,9 +42,13 @@ setup( license="BSD", install_requires=requirements, extras_require={}, + dependency_links=[ + "git+https://github.com/pyqtgraph/pyqtgraph.git@a6d5e28#egg=pyqtgraph" + ], packages=find_packages(), namespace_packages=[], test_suite="artiq.test", + package_data={"artiq": [os.path.join("gui", "icon.png")]}, ext_modules=[], entry_points={ "console_scripts": scripts, diff --git a/soc/runtime/Makefile b/soc/runtime/Makefile index 3cf2708dc..241448f37 100644 --- a/soc/runtime/Makefile +++ b/soc/runtime/Makefile @@ -1,6 +1,6 @@ include $(MSCDIR)/software/common.mak -OBJECTS := isr.o flash_storage.o clock.o elf_loader.o services.o session.o log.o test_mode.o kloader.o bridge_ctl.o mailbox.o ksupport_data.o kserver.o main.o +OBJECTS := isr.o flash_storage.o clock.o elf_loader.o services.o session.o log.o test_mode.o kloader.o bridge_ctl.o mailbox.o ksupport_data.o kserver.o moninj.o main.o OBJECTS_KSUPPORT := ksupport.o exception_jmp.o exceptions.o mailbox.o bridge.o rtio.o ttl.o dds.o CFLAGS += -Ilwip/src/include -Iliblwip diff --git a/soc/runtime/bridge.c b/soc/runtime/bridge.c index c61875236..eb16587de 100644 --- a/soc/runtime/bridge.c +++ b/soc/runtime/bridge.c @@ -66,7 +66,7 @@ void bridge_main(void) struct msg_brg_dds_sel *msg; msg = (struct msg_brg_dds_sel *)umsg; - dds_write(DDS_GPIO, msg->channel); + dds_write(DDS_GPIO, msg->channel << 1); mailbox_acknowledge(); break; } @@ -74,7 +74,7 @@ void bridge_main(void) unsigned int g; g = dds_read(DDS_GPIO); - dds_write(DDS_GPIO, g | (1 << 7)); + dds_write(DDS_GPIO, g | 1); dds_write(DDS_GPIO, g); mailbox_acknowledge(); diff --git a/soc/runtime/dds.c b/soc/runtime/dds.c index fc871fa26..5f9f8650e 100644 --- a/soc/runtime/dds.c +++ b/soc/runtime/dds.c @@ -3,12 +3,28 @@ #include "exceptions.h" #include "rtio.h" +#include "log.h" #include "dds.h" -#define DURATION_WRITE 5 +#define DURATION_WRITE (5 << RTIO_FINE_TS_WIDTH) + +#if defined DDS_AD9858 +/* Assume 8-bit bus */ #define DURATION_INIT (7*DURATION_WRITE) /* not counting FUD */ #define DURATION_PROGRAM (8*DURATION_WRITE) /* not counting FUD */ +#elif defined DDS_AD9914 +/* Assume 16-bit bus */ +/* DAC calibration takes max. 135us as per datasheet. Take a good margin. */ +#define DURATION_DAC_CAL (30000 << RTIO_FINE_TS_WIDTH) +/* not counting final FUD */ +#define DURATION_INIT (8*DURATION_WRITE + DURATION_DAC_CAL) +#define DURATION_PROGRAM (5*DURATION_WRITE) /* not counting FUD */ + +#else +#error Unknown DDS configuration +#endif + #define DDS_WRITE(addr, data) do { \ rtio_o_address_write(addr); \ rtio_o_data_write(data); \ @@ -38,47 +54,113 @@ void dds_init(long long int timestamp, int channel) now = timestamp - DURATION_INIT; +#ifdef DDS_ONEHOT_SEL + channel = 1 << channel; +#endif + channel <<= 1; DDS_WRITE(DDS_GPIO, channel); - DDS_WRITE(DDS_GPIO, channel | (1 << 7)); + DDS_WRITE(DDS_GPIO, channel | 1); /* reset */ DDS_WRITE(DDS_GPIO, channel); - DDS_WRITE(0x00, 0x78); - DDS_WRITE(0x01, 0x00); - DDS_WRITE(0x02, 0x00); - DDS_WRITE(0x03, 0x00); - +#ifdef DDS_AD9858 + /* + * 2GHz divider disable + * SYNCLK disable + * Mixer power-down + * Phase detect power down + */ + DDS_WRITE(DDS_CFR0, 0x78); + DDS_WRITE(DDS_CFR1, 0x00); + DDS_WRITE(DDS_CFR2, 0x00); + DDS_WRITE(DDS_CFR3, 0x00); DDS_WRITE(DDS_FUD, 0); +#endif + +#ifdef DDS_AD9914 + /* + * Enable cosine output (to match AD9858 behavior) + * Enable DAC calibration + * Leave SYNCLK enabled and PLL/divider disabled + */ + DDS_WRITE(DDS_CFR1L, 0x0008); + DDS_WRITE(DDS_CFR1H, 0x0000); + DDS_WRITE(DDS_CFR4H, 0x0105); + DDS_WRITE(DDS_FUD, 0); + /* Disable DAC calibration */ + now += DURATION_DAC_CAL; + DDS_WRITE(DDS_CFR4H, 0x0005); + DDS_WRITE(DDS_FUD, 0); +#endif } -static void dds_set_one(long long int now, long long int ref_time, int channel, +/* Compensation to keep phase continuity when switching from absolute or tracking + * to continuous phase mode. */ +static unsigned int continuous_phase_comp[DDS_CHANNEL_COUNT]; + +static void dds_set_one(long long int now, long long int ref_time, unsigned int channel, unsigned int ftw, unsigned int pow, int phase_mode) { - DDS_WRITE(DDS_GPIO, channel); + unsigned int channel_enc; - if(phase_mode == PHASE_MODE_CONTINUOUS) - /* Do not clear phase accumulator on FUD */ - DDS_WRITE(0x02, 0x00); - else - /* Clear phase accumulator on FUD */ - DDS_WRITE(0x02, 0x40); + if(channel >= DDS_CHANNEL_COUNT) { + log("Attempted to set invalid DDS channel"); + return; + } +#ifdef DDS_ONEHOT_SEL + channel_enc = 1 << channel; +#else + channel_enc = channel; +#endif + DDS_WRITE(DDS_GPIO, channel_enc << 1); +#ifdef DDS_AD9858 DDS_WRITE(DDS_FTW0, ftw & 0xff); DDS_WRITE(DDS_FTW1, (ftw >> 8) & 0xff); DDS_WRITE(DDS_FTW2, (ftw >> 16) & 0xff); DDS_WRITE(DDS_FTW3, (ftw >> 24) & 0xff); +#endif - /* We assume that the RTIO clock is DDS SYNCLK */ - if(phase_mode == PHASE_MODE_TRACKING) - pow += (ref_time >> RTIO_FINE_TS_WIDTH)*ftw >> 18; - if(phase_mode != PHASE_MODE_CONTINUOUS) { +#ifdef DDS_AD9914 + DDS_WRITE(DDS_FTWL, ftw & 0xffff); + DDS_WRITE(DDS_FTWH, (ftw >> 16) & 0xffff); +#endif + + /* We need the RTIO fine timestamp clock to be phase-locked + * to DDS SYSCLK, and divided by an integer DDS_RTIO_CLK_RATIO. + */ + if(phase_mode == PHASE_MODE_CONTINUOUS) { + /* Do not clear phase accumulator on FUD */ +#ifdef DDS_AD9858 + DDS_WRITE(DDS_CFR2, 0x00); +#endif +#ifdef DDS_AD9914 + DDS_WRITE(DDS_CFR1L, 0x0008); +#endif + pow += continuous_phase_comp[channel]; + } else { long long int fud_time; + /* Clear phase accumulator on FUD */ +#ifdef DDS_AD9858 + DDS_WRITE(DDS_CFR2, 0x40); +#endif +#ifdef DDS_AD9914 + DDS_WRITE(DDS_CFR1L, 0x2008); +#endif fud_time = now + 2*DURATION_WRITE; - pow -= ((ref_time - fud_time) >> RTIO_FINE_TS_WIDTH)*ftw >> 18; + pow -= (ref_time - fud_time)*DDS_RTIO_CLK_RATIO*ftw >> (32-DDS_POW_WIDTH); + if(phase_mode == PHASE_MODE_TRACKING) + pow += ref_time*DDS_RTIO_CLK_RATIO*ftw >> (32-DDS_POW_WIDTH); + continuous_phase_comp[channel] = pow; } +#ifdef DDS_AD9858 DDS_WRITE(DDS_POW0, pow & 0xff); DDS_WRITE(DDS_POW1, (pow >> 8) & 0x3f); +#endif +#ifdef DDS_AD9914 + DDS_WRITE(DDS_POW, pow); +#endif DDS_WRITE(DDS_FUD, 0); } diff --git a/soc/runtime/dds.h b/soc/runtime/dds.h index e6adc326c..6ca9d1c1e 100644 --- a/soc/runtime/dds.h +++ b/soc/runtime/dds.h @@ -2,15 +2,17 @@ #define __DDS_H #include +#include #include -/* Number of DDS channels to initialize */ -#define DDS_CHANNEL_COUNT 8 - /* Maximum number of commands in a batch */ #define DDS_MAX_BATCH 16 -/* DDS core registers */ +#ifdef DDS_AD9858 +#define DDS_CFR0 0x00 +#define DDS_CFR1 0x01 +#define DDS_CFR2 0x02 +#define DDS_CFR3 0x03 #define DDS_FTW0 0x0a #define DDS_FTW1 0x0b #define DDS_FTW2 0x0c @@ -19,6 +21,31 @@ #define DDS_POW1 0x0f #define DDS_FUD 0x40 #define DDS_GPIO 0x41 +#endif + +#ifdef DDS_AD9914 +#define DDS_CFR1L 0x01 +#define DDS_CFR1H 0x03 +#define DDS_CFR2L 0x05 +#define DDS_CFR2H 0x07 +#define DDS_CFR3L 0x09 +#define DDS_CFR3H 0x0b +#define DDS_CFR4L 0x0d +#define DDS_CFR4H 0x0f +#define DDS_FTWL 0x2d +#define DDS_FTWH 0x2f +#define DDS_POW 0x31 +#define DDS_FUD 0x80 +#define DDS_GPIO 0x81 +#endif + +#ifdef DDS_AD9858 +#define DDS_POW_WIDTH 14 +#endif + +#ifdef DDS_AD9914 +#define DDS_POW_WIDTH 16 +#endif enum { PHASE_MODE_CONTINUOUS = 0, diff --git a/soc/runtime/flash_storage.c b/soc/runtime/flash_storage.c index 430a48536..34bc3a508 100644 --- a/soc/runtime/flash_storage.c +++ b/soc/runtime/flash_storage.c @@ -3,11 +3,13 @@ */ #include +#include #include #include #include #include +#include "log.h" #include "flash_storage.h" #if (defined CSR_SPIFLASH_BASE && defined SPIFLASH_PAGE_SIZE) @@ -19,133 +21,246 @@ #define min(a, b) (a>b?b:a) #define max(a, b) (a>b?a:b) -#define goto_next_record(buff, addr) do { \ - unsigned int key_size = strlen(&buff[addr])+1; \ - if(key_size % 4) \ - key_size += 4 - (key_size % 4); \ - unsigned int *buflen_p = (unsigned int *)&buff[addr + key_size]; \ - unsigned int buflen = *buflen_p; \ - if(buflen % 4) \ - buflen += 4 - (buflen % 4); \ - addr += key_size + sizeof(int) + buflen; \ - } while (0) - -union seek { - unsigned int integer; - char bytes[4]; +struct record { + char *key; + unsigned int key_len; + char *value; + unsigned int value_len; + char *raw_record; + unsigned int size; }; -static void write_at_offset(char *key, void *buffer, int buflen, unsigned int sector_offset); -static char key_exists(char *buff, char *key, char *end); -static char check_for_duplicates(char *buff); -static unsigned int try_to_flush_duplicates(void); +struct iter_state { + char *buffer; + unsigned int seek; + unsigned int buf_len; +}; -static char key_exists(char *buff, char *key, char *end) +static unsigned int get_record_size(char *buff) { - unsigned int addr; + unsigned int record_size; - addr = 0; - while(&buff[addr] < end && *(unsigned int*)&buff[addr] != END_MARKER) { - if(strcmp(&buff[addr], key) == 0) - return 1; - goto_next_record(buff, addr); + memcpy(&record_size, buff, 4); + return record_size; +} + +static void record_iter_init(struct iter_state *is, char *buffer, unsigned int buf_len) +{ + is->buffer = buffer; + is->seek = 0; + is->buf_len = buf_len; +} + +static int record_iter_next(struct iter_state *is, struct record *record, int *fatal) +{ + if(is->seek >= is->buf_len) + return 0; + + record->raw_record = &is->buffer[is->seek]; + record->size = get_record_size(record->raw_record); + + if(record->size == END_MARKER) + return 0; + + if(record->size < 6) { + log("flash_storage might be corrupted: record size is %u (<6) at address %08x", record->size, record->raw_record); + if(fatal) + *fatal = 1; + return 0; } + + if(is->seek > is->buf_len - sizeof(record->size) - 2) { /* 2 is the minimum key length */ + log("flash_storage might be corrupted: END_MARKER missing at the end of the storage sector"); + if(fatal) + *fatal = 1; + return 0; + } + + if(record->size > is->buf_len - is->seek) { + log("flash_storage might be corrupted: invalid record_size %d at address %08x", record->size, record->raw_record); + if(fatal) + *fatal = 1; + return 0; + } + + record->key = record->raw_record + sizeof(record->size); + record->key_len = strnlen(record->key, record->size - sizeof(record->size)) + 1; + + if(record->key_len == record->size - sizeof(record->size) + 1) { + log("flash_storage might be corrupted: invalid key length at address %08x", record->raw_record); + if(fatal) + *fatal = 1; + return 0; + } + + record->value = record->key + record->key_len; + record->value_len = record->size - record->key_len - sizeof(record->size); + + is->seek += record->size; + return 1; +} + +static unsigned int get_free_space(void) +{ + struct iter_state is; + struct record record; + + record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE); + while(record_iter_next(&is, &record, NULL)); + return STORAGE_SIZE - is.seek; +} + +static int is_empty(struct record *record) +{ + return record->value_len == 0; +} + +static int key_exists(char *buff, char *key, char *end, char accept_empty, struct record *found_record) +{ + struct iter_state is; + struct record iter_record; + int found = 0; + + record_iter_init(&is, buff, end - buff); + while(record_iter_next(&is, &iter_record, NULL)) { + if(strcmp(iter_record.key, key) == 0) { + found = 1; + if(found_record) + *found_record = iter_record; + } + } + + if(found && is_empty(found_record) && !accept_empty) + return 0; + + if(found) + return 1; + return 0; } static char check_for_duplicates(char *buff) { - unsigned int addr; - char *key_name; + struct record record, following_record; + struct iter_state is; + int no_error; - addr = 0; - while(addr < STORAGE_SIZE && *(unsigned int *)&buff[addr] != END_MARKER) { - key_name = &buff[addr]; - goto_next_record(buff, addr); - if(key_exists(&buff[addr], key_name, &buff[STORAGE_SIZE])) + record_iter_init(&is, buff, STORAGE_SIZE); + no_error = record_iter_next(&is, &record, NULL); + while(no_error) { + no_error = record_iter_next(&is, &following_record, NULL); + if(no_error && key_exists(following_record.raw_record, record.key, &buff[STORAGE_SIZE], 1, NULL)) return 1; + record = following_record; } return 0; } -static unsigned int try_to_flush_duplicates(void) +static char check_for_empty_records(char *buff) { - unsigned int addr, i, key_size, buflen; - char *key_name, *last_duplicate; - char sector_buff[STORAGE_SIZE]; - union seek *seeker = (union seek *)sector_buff; + struct iter_state is; + struct record record; - memcpy(sector_buff, STORAGE_ADDRESS, STORAGE_SIZE); - if(check_for_duplicates(sector_buff)) { - fs_erase(); - for(addr = 0; addr < STORAGE_SIZE && seeker[addr >> 2].integer != END_MARKER;) { - key_name = §or_buff[addr]; - key_size = strlen(key_name)+1; - if(key_size % 4) - key_size += 4 - (key_size % 4); - if(!key_exists((char *)STORAGE_ADDRESS, key_name, STORAGE_ADDRESS+STORAGE_SIZE)) { - last_duplicate = key_name; - for(i = addr; i < STORAGE_SIZE;) { - goto_next_record(sector_buff, i); - if(strcmp(§or_buff[i], key_name) == 0) - last_duplicate = §or_buff[i]; - } - buflen = *(unsigned int *)&last_duplicate[key_size]; - fs_write(key_name, &last_duplicate[key_size+sizeof(int)], buflen); - } - goto_next_record(sector_buff, addr); - } - return 0; - } else - return 1; + record_iter_init(&is, buff, STORAGE_SIZE); + while(record_iter_next(&is, &record, NULL)) + if(is_empty(&record)) + return 1; + + return 0; } -static void write_at_offset(char *key, void *buffer, int buflen, unsigned int sector_offset) +static unsigned int try_to_flush_duplicates(char *new_key, unsigned int buf_len) +{ + unsigned int key_size, new_record_size, ret = 0, can_rollback = 0; + struct record record, previous_record; + char sector_buff[STORAGE_SIZE]; + struct iter_state is; + + memcpy(sector_buff, STORAGE_ADDRESS, STORAGE_SIZE); + if(check_for_duplicates(sector_buff) + || key_exists(sector_buff, new_key, §or_buff[STORAGE_SIZE], 0, NULL) + || check_for_empty_records(sector_buff)) { + fs_erase(); + record_iter_init(&is, sector_buff, STORAGE_SIZE); + while(record_iter_next(&is, &record, NULL)) { + if(is_empty(&record)) + continue; + if(!key_exists((char *)STORAGE_ADDRESS, record.key, STORAGE_ADDRESS + STORAGE_SIZE, 1, NULL)) { + struct record rec; + + if(!key_exists(sector_buff, record.key, §or_buff[STORAGE_SIZE], 0, &rec)) + continue; + if(strcmp(new_key, record.key) == 0) { // If we are about to write this key we don't keep the old value. + previous_record = rec; // This holds the old record in case we need it back (for instance if new record is too long) + can_rollback = 1; + } else + fs_write(record.key, rec.value, rec.value_len); + } + } + ret = 1; + } + + key_size = strlen(new_key) + 1; + new_record_size = key_size + buf_len + sizeof(new_record_size); + if(can_rollback && new_record_size > get_free_space()) { + fs_write(new_key, previous_record.value, previous_record.value_len); + } + + return ret; +} + +static void write_at_offset(char *key, void *buffer, int buf_len, unsigned int sector_offset) { int key_len = strlen(key) + 1; - int key_len_alignment = 0, buflen_alignment = 0; - unsigned char padding[3] = {0, 0, 0}; + unsigned int record_size = key_len + buf_len + sizeof(record_size); + unsigned int flash_addr = (unsigned int)STORAGE_ADDRESS + sector_offset; - if(key_len % 4) - key_len_alignment = 4 - (key_len % 4); - - if(buflen % 4) - buflen_alignment = 4 - (buflen % 4); - - write_to_flash(sector_offset, (unsigned char *)key, key_len); - write_to_flash(sector_offset+key_len, padding, key_len_alignment); - write_to_flash(sector_offset+key_len+key_len_alignment, (unsigned char *)&buflen, sizeof(buflen)); - write_to_flash(sector_offset+key_len+key_len_alignment+sizeof(buflen), buffer, buflen); - write_to_flash(sector_offset+key_len+key_len_alignment+sizeof(buflen)+buflen, padding, buflen_alignment); + write_to_flash(flash_addr, (unsigned char *)&record_size, sizeof(record_size)); + write_to_flash(flash_addr+sizeof(record_size), (unsigned char *)key, key_len); + write_to_flash(flash_addr+sizeof(record_size)+key_len, buffer, buf_len); flush_cpu_dcache(); } -void fs_write(char *key, void *buffer, unsigned int buflen) +int fs_write(char *key, void *buffer, unsigned int buf_len) { - char *addr; - unsigned int key_size = strlen(key)+1; - unsigned int record_size = key_size + sizeof(int) + buflen; + struct record record; + unsigned int key_size = strlen(key) + 1; + unsigned int new_record_size = key_size + sizeof(int) + buf_len; + int no_error, fatal = 0; + struct iter_state is; - for(addr = STORAGE_ADDRESS; addr < STORAGE_ADDRESS + STORAGE_SIZE - record_size; addr += 4) { - if(*(unsigned int *)addr == END_MARKER) { - write_at_offset(key, buffer, buflen, (unsigned int)addr); - break; - } - } - if(addr >= STORAGE_ADDRESS + STORAGE_SIZE - record_size) { // Flash is full? Try to flush duplicates. - if(try_to_flush_duplicates()) - return; // No duplicates found, cannot write the new key-value record: sector is full. + record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE); + while((no_error = record_iter_next(&is, &record, &fatal))); - // Now retrying to write, hoping enough flash was freed. - for(addr = STORAGE_ADDRESS; addr < STORAGE_ADDRESS + STORAGE_SIZE - record_size; addr += 4) { - if(*(unsigned int *)addr == END_MARKER) { - write_at_offset(key, buffer, buflen, (unsigned int)addr); - break; - } - } + if(fatal) + goto fatal_error; + + if(STORAGE_SIZE - is.seek >= new_record_size) { + write_at_offset(key, buffer, buf_len, is.seek); + return 1; } + + if(!try_to_flush_duplicates(key, buf_len)) // storage is full, let's try to free some space up. + return 0; // No duplicates found, cannot write the new key-value record: sector is full. + // Now retrying to write, hoping enough flash was freed. + + record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE); + while((no_error = record_iter_next(&is, &record, &fatal))); + + if(fatal) + goto fatal_error; + + if(STORAGE_SIZE - is.seek >= new_record_size) { + write_at_offset(key, buffer, buf_len, is.seek); + return 1; // We eventually succeeded in writing the record + } else + return 0; // Storage is definitely full. + +fatal_error: + log("fatal error: flash storage might be corrupted"); + return 0; } void fs_erase(void) @@ -154,33 +269,35 @@ void fs_erase(void) flush_cpu_dcache(); } -unsigned int fs_read(char *key, void *buffer, unsigned int buflen, unsigned int *remain) +unsigned int fs_read(char *key, void *buffer, unsigned int buf_len, unsigned int *remain) { unsigned int read_length = 0; - char *addr; + struct iter_state is; + struct record record; + int fatal = 0; - addr = STORAGE_ADDRESS; - while(addr < (STORAGE_ADDRESS + STORAGE_SIZE) && (*addr != END_MARKER)) { - unsigned int key_len, value_len; - char *key_addr = addr; + if(remain) + *remain = 0; - key_len = strlen(addr) + 1; - if(key_len % 4) - key_len += 4 - (key_len % 4); - addr += key_len; - value_len = *(unsigned int *)addr; - addr += sizeof(value_len); - if(strcmp(key_addr, key) == 0) { - memcpy(buffer, addr, min(value_len, buflen)); - read_length = min(value_len, buflen); + record_iter_init(&is, STORAGE_ADDRESS, STORAGE_SIZE); + while(record_iter_next(&is, &record, &fatal)) { + if(strcmp(record.key, key) == 0) { + memcpy(buffer, record.value, min(record.value_len, buf_len)); + read_length = min(record.value_len, buf_len); if(remain) - *remain = max(0, (int)value_len - (int)buflen); + *remain = max(0, (int)record.value_len - (int)buf_len); } - addr += value_len; - if((int)addr % 4) - addr += 4 - ((int)addr % 4); } + + if(fatal) + log("fatal error: flash storage might be corrupted"); + return read_length; } +void fs_remove(char *key) +{ + fs_write(key, NULL, 0); +} + #endif /* CSR_SPIFLASH_BASE && SPIFLASH_PAGE_SIZE */ diff --git a/soc/runtime/flash_storage.h b/soc/runtime/flash_storage.h index 065dd82f3..9994fef37 100644 --- a/soc/runtime/flash_storage.h +++ b/soc/runtime/flash_storage.h @@ -5,8 +5,9 @@ #ifndef __FLASH_STORAGE_H #define __FLASH_STORAGE_H +void fs_remove(char *key); void fs_erase(void); -void fs_write(char *key, void *buffer, unsigned int buflen); +int fs_write(char *key, void *buffer, unsigned int buflen); unsigned int fs_read(char *key, void *buffer, unsigned int buflen, unsigned int *remain); #endif /* __FLASH_STORAGE_H */ diff --git a/soc/runtime/gen_service_table.py b/soc/runtime/gen_service_table.py index b194662cd..27ae562b0 100755 --- a/soc/runtime/gen_service_table.py +++ b/soc/runtime/gen_service_table.py @@ -21,6 +21,7 @@ services = [ ("ttl_set_oe", "ttl_set_oe"), ("ttl_set_sensitivity", "ttl_set_sensitivity"), ("ttl_get", "ttl_get"), + ("ttl_clock_set", "ttl_clock_set"), ("dds_init", "dds_init"), ("dds_batch_enter", "dds_batch_enter"), diff --git a/soc/runtime/ksupport.c b/soc/runtime/ksupport.c index a2eb090c6..032fe88ec 100644 --- a/soc/runtime/ksupport.c +++ b/soc/runtime/ksupport.c @@ -79,7 +79,7 @@ long long int now_init(void) if(now < 0) { rtio_init(); - now = 125000; + now = rtio_get_counter() + 125000; } return now; diff --git a/soc/runtime/liblwip/lwipopts.h b/soc/runtime/liblwip/lwipopts.h index 14c2c2677..eff5a4aae 100644 --- a/soc/runtime/liblwip/lwipopts.h +++ b/soc/runtime/liblwip/lwipopts.h @@ -66,7 +66,7 @@ a lot of data that needs to be copied, this should be set high. */ #define MEMP_NUM_PBUF 64 /* MEMP_NUM_UDP_PCB: the number of UDP protocol control blocks. One per active UDP "connection". */ -#define MEMP_NUM_UDP_PCB 1 +#define MEMP_NUM_UDP_PCB 2 /* MEMP_NUM_TCP_PCB: the number of simulatenously active TCP connections. */ #define MEMP_NUM_TCP_PCB 8 @@ -159,7 +159,7 @@ a lot of data that needs to be copied, this should be set high. */ #define DHCP_DOES_ARP_CHECK 0 /* ---------- UDP options ---------- */ -#define LWIP_UDP 0 +#define LWIP_UDP 1 #define UDP_TTL 255 diff --git a/soc/runtime/lwip b/soc/runtime/lwip index 737a6921c..d3217718a 160000 --- a/soc/runtime/lwip +++ b/soc/runtime/lwip @@ -1 +1 @@ -Subproject commit 737a6921c3fd9ef6ace17685b208b1af394f42b6 +Subproject commit d3217718a904ffd3b6f6cd12d14d526813d46d7a diff --git a/soc/runtime/mailbox.c b/soc/runtime/mailbox.c index 2f0786893..98e49045b 100644 --- a/soc/runtime/mailbox.c +++ b/soc/runtime/mailbox.c @@ -29,22 +29,8 @@ static void _flush_cpu_dcache(void) mtspr(SPR_DCBIR, i); } -/* TODO: do not use L2 cache in AMP systems */ -static void _flush_l2_cache(void) -{ - unsigned int i; - register unsigned int addr; - register unsigned int dummy; - - for(i=0;i<2*8192/4;i++) { - addr = 0x40000000 + i*4; - __asm__ volatile("l.lwz %0, 0(%1)\n":"=r"(dummy):"r"(addr)); - } -} - void mailbox_send(void *ptr) { - _flush_l2_cache(); last_transmission = (unsigned int)ptr; KERNELCPU_MAILBOX = last_transmission; } @@ -72,7 +58,6 @@ void *mailbox_receive(void) return NULL; else { if(r) { - _flush_l2_cache(); _flush_cpu_dcache(); } return (void *)r; diff --git a/soc/runtime/main.c b/soc/runtime/main.c index 95889fd07..4feb8c9a0 100644 --- a/soc/runtime/main.c +++ b/soc/runtime/main.c @@ -4,7 +4,6 @@ #include #include #include -#include #include #include @@ -28,10 +27,10 @@ #include "test_mode.h" #include "kserver.h" #include "session.h" +#include "moninj.h" static void common_init(void) { - clock_init(); brg_start(); brg_ddsinitall(); kloader_stop(); @@ -139,6 +138,7 @@ static void regular_main(void) puts("Accepting sessions on Ethernet."); network_init(); kserver_init(); + moninj_init(); session_end(); while(1) { @@ -149,16 +149,18 @@ static void regular_main(void) #else /* CSR_ETHMAC_BASE */ -static void reset_serial_session(void) +static void reset_serial_session(int signal) { int i; session_end(); - /* Signal end-of-session inband with zero length packet. */ - for(i=0;i<4;i++) - uart_write(0x5a); - for(i=0;i<4;i++) - uart_write(0x00); + if(signal) { + /* Signal end-of-session inband with zero length packet. */ + for(i=0;i<4;i++) + uart_write(0x5a); + for(i=0;i<4;i++) + uart_write(0x00); + } session_start(); } @@ -179,7 +181,8 @@ static void serial_service(void) if(r > 0) rxpending = 0; if(r < 0) - reset_serial_session(); + /* do not signal if reset was requested by host */ + reset_serial_session(r != -2); } session_poll((void **)&txdata, &txlen); @@ -189,7 +192,7 @@ static void serial_service(void) session_ack_data(txlen); session_ack_mem(txlen); } else if(txlen < 0) - reset_serial_session(); + reset_serial_session(1); } static void regular_main(void) @@ -206,34 +209,31 @@ static void regular_main(void) static void blink_led(void) { - int i, ev, p; + int i; + long long int t; - p = identifier_frequency_read()/10; - time_init(); for(i=0;i<3;i++) { leds_out_write(1); - while(!elapsed(&ev, p)); + t = clock_get_ms(); + while(clock_get_ms() < t + 250); leds_out_write(0); - while(!elapsed(&ev, p)); + t = clock_get_ms(); + while(clock_get_ms() < t + 250); } } static int check_test_mode(void) { char c; + long long int t; - timer0_en_write(0); - timer0_reload_write(0); - timer0_load_write(identifier_frequency_read() >> 2); - timer0_en_write(1); - timer0_update_value_write(1); - while(timer0_value_read()) { + t = clock_get_ms(); + while(clock_get_ms() < t + 1000) { if(readchar_nonblock()) { c = readchar(); if((c == 't')||(c == 'T')) return 1; } - timer0_update_value_write(1); } return 0; } @@ -246,6 +246,7 @@ int main(void) puts("ARTIQ runtime built "__DATE__" "__TIME__"\n"); + clock_init(); puts("Press 't' to enter test mode..."); blink_led(); diff --git a/soc/runtime/moninj.c b/soc/runtime/moninj.c new file mode 100644 index 000000000..d7edff77d --- /dev/null +++ b/soc/runtime/moninj.c @@ -0,0 +1,156 @@ +#include + +#ifdef CSR_ETHMAC_BASE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "log.h" +#include "moninj.h" + +enum { + MONINJ_REQ_MONITOR = 1, + MONINJ_REQ_TTLSET = 2 +}; + +enum { + MONINJ_TTL_MODE_EXP = 0, + MONINJ_TTL_MODE_1 = 1, + MONINJ_TTL_MODE_0 = 2, + MONINJ_TTL_MODE_IN = 3 +}; + +enum { + MONINJ_TTL_OVERRIDE_ENABLE = 0, + MONINJ_TTL_OVERRIDE_O = 1, + MONINJ_TTL_OVERRIDE_OE = 2 +}; + +static struct udp_pcb *listen_pcb; + +struct monitor_reply { + long long int ttl_levels; + long long int ttl_oes; + long long int ttl_overrides; + unsigned int dds_ftws[DDS_CHANNEL_COUNT]; +}; + +static void moninj_monitor(const ip_addr_t *addr, u16_t port) +{ + struct monitor_reply reply; + int i; + struct pbuf *reply_p; + + reply.ttl_levels = 0; + reply.ttl_oes = 0; + reply.ttl_overrides = 0; + for(i=0;ipayload, &reply, sizeof(struct monitor_reply)); + udp_sendto(listen_pcb, reply_p, addr, port); + pbuf_free(reply_p); +} + +static void moninj_ttlset(int channel, int mode) +{ + rtio_moninj_inj_chan_sel_write(channel); + switch(mode) { + case MONINJ_TTL_MODE_EXP: + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_ENABLE); + rtio_moninj_inj_value_write(0); + break; + case MONINJ_TTL_MODE_1: + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_O); + rtio_moninj_inj_value_write(1); + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_OE); + rtio_moninj_inj_value_write(1); + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_ENABLE); + rtio_moninj_inj_value_write(1); + break; + case MONINJ_TTL_MODE_0: + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_O); + rtio_moninj_inj_value_write(0); + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_OE); + rtio_moninj_inj_value_write(1); + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_ENABLE); + rtio_moninj_inj_value_write(1); + break; + case MONINJ_TTL_MODE_IN: + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_OE); + rtio_moninj_inj_value_write(0); + rtio_moninj_inj_override_sel_write(MONINJ_TTL_OVERRIDE_ENABLE); + rtio_moninj_inj_value_write(1); + break; + default: + log("unknown TTL mode %d", mode); + break; + } +} + +static void moninj_recv(void *arg, struct udp_pcb *upcb, struct pbuf *req, + const ip_addr_t *addr, u16_t port) +{ + char *p = (char *)req->payload; + + if(req->len >= 1) { + switch(p[0]) { + case MONINJ_REQ_MONITOR: + moninj_monitor(addr, port); + break; + case MONINJ_REQ_TTLSET: + if(req->len < 3) + break; + moninj_ttlset(p[1], p[2]); + break; + default: + break; + } + } + pbuf_free(req); /* beware: addr may point into the req pbuf */ +} + +void moninj_init(void) +{ + listen_pcb = udp_new(); + if(!listen_pcb) { + log("Failed to create UDP listening PCB"); + return; + } + udp_bind(listen_pcb, IP_ADDR_ANY, 3250); + udp_recv(listen_pcb, moninj_recv, NULL); +} + +#endif /* CSR_ETHMAC_BASE */ diff --git a/soc/runtime/moninj.h b/soc/runtime/moninj.h new file mode 100644 index 000000000..1224b3b6e --- /dev/null +++ b/soc/runtime/moninj.h @@ -0,0 +1,6 @@ +#ifndef __MONINJ_H +#define __MONINJ_H + +void moninj_init(void); + +#endif /* __MONINJ_H */ diff --git a/soc/runtime/rtio.c b/soc/runtime/rtio.c index 0e5cdad10..3329019ba 100644 --- a/soc/runtime/rtio.c +++ b/soc/runtime/rtio.c @@ -14,4 +14,3 @@ long long int rtio_get_counter(void) rtio_counter_update_write(1); return rtio_counter_read(); } - diff --git a/soc/runtime/services.c b/soc/runtime/services.c index 4e5292506..74bdeef71 100644 --- a/soc/runtime/services.c +++ b/soc/runtime/services.c @@ -17,8 +17,8 @@ extern __divsi3, __modsi3, __ledf2, __gedf2, __unorddf2, __eqdf2, __ltdf2, __divsf3, __lshrdi3, __muldi3, __divdi3, __ashldi3, __ashrdi3, __udivmoddi4, __floatsisf, __floatunsisf, __fixsfsi, __fixunssfsi, __adddf3, __subdf3, __muldf3, __divdf3, __floatsidf, __floatunsidf, - __floatdidf, __fixdfsi, __fixunsdfsi, __clzsi2, __ctzsi2, __udivdi3, - __umoddi3, __moddi3; + __floatdidf, __fixdfsi, __fixdfdi, __fixunsdfsi, __clzsi2, __ctzsi2, + __udivdi3, __umoddi3, __moddi3; #pragma GCC diagnostic pop static const struct symbol compiler_rt[] = { @@ -55,6 +55,7 @@ static const struct symbol compiler_rt[] = { {"floatunsidf", &__floatunsidf}, {"floatdidf", &__floatdidf}, {"fixdfsi", &__fixdfsi}, + {"fixdfdi", &__fixdfdi}, {"fixunsdfsi", &__fixunsdfsi}, {"clzsi2", &__clzsi2}, {"ctzsi2", &__ctzsi2}, diff --git a/soc/runtime/session.c b/soc/runtime/session.c index a0f2d3ab7..1a7f5d879 100644 --- a/soc/runtime/session.c +++ b/soc/runtime/session.c @@ -12,6 +12,7 @@ #include "kloader.h" #include "exceptions.h" #include "session.h" +#include "flash_storage.h" #define BUFFER_IN_SIZE (1024*1024) #define BUFFER_OUT_SIZE (1024*1024) @@ -69,11 +70,13 @@ void session_start(void) memset(&buffer_out[4], 0, 4); kloader_stop(); user_kernel_state = USER_KERNEL_NONE; + now = -1; } void session_end(void) { kloader_stop(); + now = -1; kloader_start_idle_kernel(); } @@ -86,7 +89,12 @@ enum { REMOTEMSG_TYPE_LOAD_OBJECT, REMOTEMSG_TYPE_RUN_KERNEL, - REMOTEMSG_TYPE_RPC_REPLY + REMOTEMSG_TYPE_RPC_REPLY, + + REMOTEMSG_TYPE_FLASH_READ_REQUEST, + REMOTEMSG_TYPE_FLASH_WRITE_REQUEST, + REMOTEMSG_TYPE_FLASH_ERASE_REQUEST, + REMOTEMSG_TYPE_FLASH_REMOVE_REQUEST }; /* device to host */ @@ -104,8 +112,23 @@ enum { REMOTEMSG_TYPE_KERNEL_EXCEPTION, REMOTEMSG_TYPE_RPC_REQUEST, + + REMOTEMSG_TYPE_FLASH_READ_REPLY, + REMOTEMSG_TYPE_FLASH_OK_REPLY, + REMOTEMSG_TYPE_FLASH_ERROR_REPLY }; +static int check_flash_storage_key_len(char *key, unsigned int key_len) +{ + if(key_len == get_in_packet_len() - 8) { + log("Invalid key: not a null-terminated string"); + buffer_out[8] = REMOTEMSG_TYPE_FLASH_ERROR_REPLY; + submit_output(9); + return 0; + } + return 1; +} + static int process_input(void) { switch(buffer_in[8]) { @@ -132,7 +155,7 @@ static int process_input(void) submit_output(9); break; } - rtiocrg_clock_sel_write(buffer_in[9]); + rtio_crg_clock_sel_write(buffer_in[9]); buffer_out[8] = REMOTEMSG_TYPE_CLOCK_SWITCH_COMPLETED; submit_output(9); break; @@ -168,10 +191,7 @@ static int process_input(void) } buffer_in[buffer_in_index] = 0; - if(buffer_in[9]) - now = -1; - - k = kloader_find((char *)&buffer_in[10]); + k = kloader_find((char *)&buffer_in[9]); if(k == NULL) { log("Failed to find kernel entry point '%s' in object", &buffer_in[9]); buffer_out[8] = REMOTEMSG_TYPE_KERNEL_STARTUP_FAILED; @@ -196,14 +216,68 @@ static int process_input(void) memcpy(&reply.eid, &buffer_in[9], 4); memcpy(&reply.retval, &buffer_in[13], 4); mailbox_send_and_wait(&reply); - /* HACK/FIXME: workaround for intermittent crashes that happen when running rpc_timing with comm_tcp */ - int i; - for(i=0;i<100000;i++) - __asm__ volatile("l.nop"); - /* */ user_kernel_state = USER_KERNEL_RUNNING; break; } + case REMOTEMSG_TYPE_FLASH_READ_REQUEST: { +#if SPIFLASH_SECTOR_SIZE - 4 > BUFFER_OUT_SIZE - 9 +#error Output buffer cannot hold the flash storage data +#elif SPIFLASH_SECTOR_SIZE - 4 > BUFFER_IN_SIZE - 9 +#error Input buffer cannot hold the flash storage data +#endif + unsigned int ret, in_packet_len; + char *key; + + in_packet_len = get_in_packet_len(); + key = &buffer_in[9]; + buffer_in[in_packet_len] = '\0'; + + buffer_out[8] = REMOTEMSG_TYPE_FLASH_READ_REPLY; + ret = fs_read(key, &buffer_out[9], sizeof(buffer_out) - 9, NULL); + submit_output(9 + ret); + break; + } + case REMOTEMSG_TYPE_FLASH_WRITE_REQUEST: { + char *key, *value; + unsigned int key_len, value_len, in_packet_len; + int ret; + + in_packet_len = get_in_packet_len(); + key = &buffer_in[9]; + key_len = strnlen(key, in_packet_len - 9) + 1; + if(!check_flash_storage_key_len(key, key_len)) + break; + + value_len = in_packet_len - key_len - 9; + value = key + key_len; + ret = fs_write(key, value, value_len); + + if(ret) + buffer_out[8] = REMOTEMSG_TYPE_FLASH_OK_REPLY; + else + buffer_out[8] = REMOTEMSG_TYPE_FLASH_ERROR_REPLY; + submit_output(9); + break; + } + case REMOTEMSG_TYPE_FLASH_ERASE_REQUEST: { + fs_erase(); + buffer_out[8] = REMOTEMSG_TYPE_FLASH_OK_REPLY; + submit_output(9); + break; + } + case REMOTEMSG_TYPE_FLASH_REMOVE_REQUEST: { + char *key; + unsigned int in_packet_len; + + in_packet_len = get_in_packet_len(); + key = &buffer_in[9]; + buffer_in[in_packet_len] = '\0'; + + fs_remove(key); + buffer_out[8] = REMOTEMSG_TYPE_FLASH_OK_REPLY; + submit_output(9); + break; + } default: return 0; } @@ -236,6 +310,9 @@ int session_input(void *data, int len) /* receiving length */ buffer_in[buffer_in_index++] = _data[consumed]; consumed++; len--; + if((buffer_in_index == 8) && (get_in_packet_len() == 0)) + /* zero-length packet = session reset */ + return -2; } else { /* receiving payload */ int packet_len; @@ -390,12 +467,14 @@ static int send_rpc_request(int rpc_num, va_list args) /* assumes output buffer is empty when called */ static int process_kmsg(struct msg_base *umsg) { - if(user_kernel_state != USER_KERNEL_RUNNING) { - log("Received message from kernel CPU while not in running state"); - return 0; - } if(!validate_kpointer(umsg)) return 0; + if((user_kernel_state != USER_KERNEL_RUNNING) + && (umsg->type != MESSAGE_TYPE_NOW_INIT_REQUEST) + && (umsg->type != MESSAGE_TYPE_NOW_SAVE)) { + log("Received unexpected message from kernel CPU while not in running state"); + return 0; + } switch(umsg->type) { case MESSAGE_TYPE_NOW_INIT_REQUEST: { @@ -462,7 +541,7 @@ static int process_kmsg(struct msg_base *umsg) case MESSAGE_TYPE_LOG: { struct msg_log *msg = (struct msg_log *)umsg; - log(msg->fmt, msg->args); + log_va(msg->fmt, msg->args); mailbox_acknowledge(); break; } diff --git a/soc/runtime/test_mode.c b/soc/runtime/test_mode.c index 0b0ee6426..3c0ce02f0 100644 --- a/soc/runtime/test_mode.c +++ b/soc/runtime/test_mode.c @@ -10,6 +10,7 @@ #include "dds.h" #include "flash_storage.h" #include "bridge_ctl.h" +#include "clock.h" #include "test_mode.h" static void leds(char *value) @@ -47,7 +48,7 @@ static void clksrc(char *value) return; } - rtiocrg_clock_sel_write(value2); + rtio_crg_clock_sel_write(value2); } static void ttloe(char *n, char *value) @@ -114,6 +115,9 @@ static void ddssel(char *n) return; } +#ifdef DDS_ONEHOT_SEL + n2 = 1 << n2; +#endif brg_ddssel(n2); } @@ -157,7 +161,12 @@ static void ddsr(char *addr) return; } +#ifdef DDS_AD9858 printf("0x%02x\n", brg_ddsread(addr2)); +#endif +#ifdef DDS_AD9914 + printf("0x%04x\n", brg_ddsread(addr2)); +#endif } static void ddsfud(void) @@ -186,11 +195,22 @@ static void ddsftw(char *n, char *ftw) return; } +#ifdef DDS_ONEHOT_SEL + n2 = 1 << n2; +#endif brg_ddssel(n2); + +#ifdef DDS_AD9858 brg_ddswrite(DDS_FTW0, ftw2 & 0xff); brg_ddswrite(DDS_FTW1, (ftw2 >> 8) & 0xff); brg_ddswrite(DDS_FTW2, (ftw2 >> 16) & 0xff); brg_ddswrite(DDS_FTW3, (ftw2 >> 24) & 0xff); +#endif +#ifdef DDS_AD9914 + brg_ddswrite(DDS_FTWL, ftw2 & 0xffff); + brg_ddswrite(DDS_FTWH, (ftw2 >> 16) & 0xffff); +#endif + brg_ddsfud(); } @@ -199,15 +219,34 @@ static void ddsreset(void) brg_ddsreset(); } +#ifdef DDS_AD9858 static void ddsinit(void) { brg_ddsreset(); - brg_ddswrite(0x00, 0x78); - brg_ddswrite(0x01, 0x00); - brg_ddswrite(0x02, 0x00); - brg_ddswrite(0x03, 0x00); + brg_ddswrite(DDS_CFR0, 0x78); + brg_ddswrite(DDS_CFR1, 0x00); + brg_ddswrite(DDS_CFR2, 0x00); + brg_ddswrite(DDS_CFR3, 0x00); brg_ddsfud(); } +#endif + +#ifdef DDS_AD9914 +static void ddsinit(void) +{ + long long int t; + + brg_ddsreset(); + brg_ddswrite(DDS_CFR1L, 0x0008); + brg_ddswrite(DDS_CFR1H, 0x0000); + brg_ddswrite(DDS_CFR4H, 0x0105); + brg_ddswrite(DDS_FUD, 0); + t = clock_get_ms(); + while(clock_get_ms() < t + 2); + brg_ddswrite(DDS_CFR4H, 0x0005); + brg_ddsfud(); +} +#endif static void ddstest_one(unsigned int i) { @@ -223,15 +262,27 @@ static void ddstest_one(unsigned int i) for(j=0; j<12; j++) { f = v[j]; - brg_ddswrite(0x0a, f & 0xff); - brg_ddswrite(0x0b, (f >> 8) & 0xff); - brg_ddswrite(0x0c, (f >> 16) & 0xff); - brg_ddswrite(0x0d, (f >> 24) & 0xff); +#ifdef DDS_AD9858 + brg_ddswrite(DDS_FTW0, f & 0xff); + brg_ddswrite(DDS_FTW1, (f >> 8) & 0xff); + brg_ddswrite(DDS_FTW2, (f >> 16) & 0xff); + brg_ddswrite(DDS_FTW3, (f >> 24) & 0xff); +#endif +#ifdef DDS_AD9914 + brg_ddswrite(DDS_FTWL, f & 0xffff); + brg_ddswrite(DDS_FTWH, (f >> 16) & 0xffff); +#endif brg_ddsfud(); - g = brg_ddsread(0x0a); - g |= brg_ddsread(0x0b) << 8; - g |= brg_ddsread(0x0c) << 16; - g |= brg_ddsread(0x0d) << 24; +#ifdef DDS_AD9858 + g = brg_ddsread(DDS_FTW0); + g |= brg_ddsread(DDS_FTW1) << 8; + g |= brg_ddsread(DDS_FTW2) << 16; + g |= brg_ddsread(DDS_FTW3) << 24; +#endif +#ifdef DDS_AD9914 + g = brg_ddsread(DDS_FTWL); + g |= brg_ddsread(DDS_FTWH) << 16; +#endif if(g != f) printf("readback fail on DDS %d, 0x%08x != 0x%08x\n", i, g, f); } @@ -259,13 +310,201 @@ static void ddstest(char *n) #if (defined CSR_SPIFLASH_BASE && defined SPIFLASH_PAGE_SIZE) static void fsread(char *key) { - char buf[256]; + char readbuf[SPIFLASH_SECTOR_SIZE]; int r; - r = fs_read(key, buf, sizeof(buf)-1, NULL); - buf[r] = 0; - puts(buf); + r = fs_read(key, readbuf, sizeof(readbuf)-1, NULL); + readbuf[r] = 0; + if(r == 0) + printf("key %s does not exist\n", key); + else + puts(readbuf); } + +static void fswrite(char *key, void *buffer, unsigned int length) +{ + if(!fs_write(key, buffer, length)) + printf("cannot write key %s because flash storage is full\n", key); +} + +static void fsfull(void) +{ + int i; + char value[4096]; + memset(value, '@', sizeof(value)); + + for(i = 0; i < SPIFLASH_SECTOR_SIZE/sizeof(value); i++) + fs_write("plip", value, sizeof(value)); +} + +static void check_read(char *key, char *expected, unsigned int length, unsigned int testnum) +{ + char readbuf[SPIFLASH_SECTOR_SIZE]; + unsigned int remain, readlength; + + memset(readbuf, '\0', sizeof(readbuf)); + + readlength = fs_read(key, readbuf, sizeof(readbuf), &remain); + if(remain > 0) + printf("KO[%u] remain == %u, expected 0\n", testnum, remain); + if(readlength != length) + printf("KO[%u] read length == %u, expected %u\n", testnum, readlength, length); + if(remain == 0 && readlength == length) + printf("."); + + readbuf[readlength] = 0; + if(memcmp(expected, readbuf, readlength) == 0) + printf(".\n"); + else + printf("KO[%u] read %s instead of %s\n", testnum, readbuf, expected); +} + +static void check_doesnt_exist(char *key, unsigned int testnum) +{ + char readbuf; + unsigned int remain, readlength; + + readlength = fs_read(key, &readbuf, sizeof(readbuf), &remain); + if(remain > 0) + printf("KO[%u] remain == %u, expected 0\n", testnum, remain); + if(readlength > 0) + printf("KO[%u] readlength == %d, expected 0\n", testnum, readlength); + if(remain == 0 && readlength == 0) + printf(".\n"); +} + +static void check_write(unsigned int ret) +{ + if(!ret) + printf("KO"); + else + printf("."); +} + +static inline void test_sector_is_full(void) +{ + char c; + char value[4096]; + char key[2] = {0, 0}; + + fs_erase(); + memset(value, '@', sizeof(value)); + for(c = 1; c <= SPIFLASH_SECTOR_SIZE/sizeof(value); c++) { + key[0] = c; + check_write(fs_write(key, value, sizeof(value) - 6)); + } + check_write(!fs_write("this_should_fail", "fail", 5)); + printf("\n"); +} + +static void test_one_big_record(int testnum) +{ + char value[SPIFLASH_SECTOR_SIZE]; + memset(value, '@', sizeof(value)); + + fs_erase(); + check_write(fs_write("a", value, sizeof(value) - 6)); + check_read("a", value, sizeof(value) - 6, testnum); + check_write(fs_write("a", value, sizeof(value) - 6)); + check_read("a", value, sizeof(value) - 6, testnum); + check_write(!fs_write("b", value, sizeof(value) - 6)); + check_read("a", value, sizeof(value) - 6, testnum); + fs_remove("a"); + check_doesnt_exist("a", testnum); + check_write(fs_write("a", value, sizeof(value) - 6)); + check_read("a", value, sizeof(value) - 6, testnum); + fs_remove("a"); + check_doesnt_exist("a", testnum); + value[0] = '!'; + check_write(fs_write("b", value, sizeof(value) - 6)); + check_read("b", value, sizeof(value) - 6, testnum); +} + +static void test_flush_duplicate_rollback(int testnum) +{ + char value[SPIFLASH_SECTOR_SIZE]; + memset(value, '@', sizeof(value)); + + fs_erase(); + /* This makes the flash storage full with one big record */ + check_write(fs_write("a", value, SPIFLASH_SECTOR_SIZE - 6)); + /* This should trigger the try_to_flush_duplicate code which + * at first will not keep the old "a" record value because we are + * overwriting it. But then it should roll back to the old value + * because the new record is too large. + */ + value[0] = '!'; + check_write(!fs_write("a", value, sizeof(value))); + /* check we still have the old record value */ + value[0] = '@'; + check_read("a", value, SPIFLASH_SECTOR_SIZE - 6, testnum); +} + +static void test_too_big_fails(int testnum) +{ + char value[SPIFLASH_SECTOR_SIZE]; + memset(value, '@', sizeof(value)); + + fs_erase(); + check_write(!fs_write("a", value, sizeof(value) - 6 + /* TOO BIG */ 1)); + check_doesnt_exist("a", testnum); +} + +static void fs_test(void) +{ + int i; + char writebuf[] = "abcdefghijklmnopqrst"; + char read_check[4096]; + int vect_length = sizeof(writebuf); + + memset(read_check, '@', sizeof(read_check)); + printf("testing...\n"); + for(i = 0; i < vect_length; i++) { + printf("%u.0:", i); + fs_erase(); + check_write(fs_write("a", writebuf, i)); + check_read("a", writebuf, i, i); + + printf("%u.1:", i); + fsfull(); + check_read("a", writebuf, i, i); + + printf("%u.2:", i); + check_read("plip", read_check, sizeof(read_check), i); + + printf("%u.3:", i); + check_write(fs_write("a", "b", 2)); + check_read("a", "b", 2, i); + + printf("%u.4:", i); + fsfull(); + check_read("a", "b", 2, i); + + printf("%u.5:", i); + check_doesnt_exist("notfound", i); + + printf("%u.6:", i); + fs_remove("a"); + check_doesnt_exist("a", i); + + printf("%u.7:", i); + fsfull(); + check_doesnt_exist("a", i); + } + + printf("%u:", vect_length); + test_sector_is_full(); + + printf("%u:", vect_length+1); + test_one_big_record(vect_length+1); + + printf("%u:", vect_length+2); + test_flush_duplicate_rollback(vect_length+2); + + printf("%u:", vect_length+3); + test_too_big_fails(vect_length+3); +} + #endif static void help(void) @@ -288,6 +527,8 @@ static void help(void) puts("fserase - erase flash storage"); puts("fswrite - write to flash storage"); puts("fsread - read flash storage"); + puts("fsremove - remove a key-value record from flash storage"); + puts("fstest - run flash storage tests. WARNING: erases the storage area"); #endif } @@ -340,6 +581,7 @@ static char *get_token(char **str) return d; } + static void do_command(char *c) { char *token; @@ -365,8 +607,10 @@ static void do_command(char *c) #if (defined CSR_SPIFLASH_BASE && defined SPIFLASH_PAGE_SIZE) else if(strcmp(token, "fserase") == 0) fs_erase(); - else if(strcmp(token, "fswrite") == 0) fs_write(get_token(&c), c, strlen(c)); + else if(strcmp(token, "fswrite") == 0) fswrite(get_token(&c), c, strlen(c)); else if(strcmp(token, "fsread") == 0) fsread(get_token(&c)); + else if(strcmp(token, "fsremove") == 0) fs_remove(get_token(&c)); + else if(strcmp(token, "fstest") == 0) fs_test(); #endif else if(strcmp(token, "") != 0) diff --git a/soc/runtime/ttl.c b/soc/runtime/ttl.c index f28faadc7..387b977b1 100644 --- a/soc/runtime/ttl.c +++ b/soc/runtime/ttl.c @@ -56,3 +56,11 @@ long long int ttl_get(int channel, long long int time_limit) rtio_i_re_write(1); return r; } + +void ttl_clock_set(long long int timestamp, int channel, int ftw) +{ + rtio_chan_sel_write(channel); + rtio_o_timestamp_write(timestamp); + rtio_o_data_write(ftw); + rtio_write_and_process_status(timestamp, channel); +} diff --git a/soc/runtime/ttl.h b/soc/runtime/ttl.h index a7d8cab49..9d95a32f2 100644 --- a/soc/runtime/ttl.h +++ b/soc/runtime/ttl.h @@ -5,5 +5,6 @@ void ttl_set_o(long long int timestamp, int channel, int value); void ttl_set_oe(long long int timestamp, int channel, int oe); void ttl_set_sensitivity(long long int timestamp, int channel, int sensitivity); long long int ttl_get(int channel, long long int time_limit); +void ttl_clock_set(long long int timestamp, int channel, int ftw); #endif /* __TTL_H */ diff --git a/soc/targets/artiq_kc705.py b/soc/targets/artiq_kc705.py index b42f445c7..cfc9774f9 100644 --- a/soc/targets/artiq_kc705.py +++ b/soc/targets/artiq_kc705.py @@ -6,12 +6,12 @@ from mibuild.xilinx.vivado import XilinxVivadoToolchain from misoclib.com import gpio from misoclib.soc import mem_decoder +from misoclib.mem.sdram.core.minicon import MiniconSettings from targets.kc705 import MiniSoC from artiq.gateware.soc import AMPSoC -from artiq.gateware import rtio, ad9858, nist_qc1 -from artiq.gateware.rtio.phy import ttl_simple -from artiq.gateware.rtio.phy.wishbone import RT2WB +from artiq.gateware import rtio, nist_qc1, nist_qc2 +from artiq.gateware.rtio.phy import ttl_simple, dds class _RTIOCRG(Module, AutoCSR): @@ -32,11 +32,12 @@ class _RTIOCRG(Module, AutoCSR): o_O=self.cd_rtio.clk) -class NIST_QC1(MiniSoC, AMPSoC): +class _NIST_QCx(MiniSoC, AMPSoC): csr_map = { "rtio": None, # mapped on Wishbone instead - "rtiocrg": 13, - "kernel_cpu": 14 + "rtio_crg": 13, + "kernel_cpu": 14, + "rtio_moninj": 15 } csr_map.update(MiniSoC.csr_map) mem_map = { @@ -47,58 +48,30 @@ class NIST_QC1(MiniSoC, AMPSoC): def __init__(self, platform, cpu_type="or1k", **kwargs): MiniSoC.__init__(self, platform, - cpu_type=cpu_type, with_timer=False, **kwargs) + cpu_type=cpu_type, + sdram_controller_settings=MiniconSettings(l2_size=128*1024), + with_timer=False, **kwargs) AMPSoC.__init__(self) - platform.add_extension(nist_qc1.fmc_adapter_io) - self.submodules.leds = gpio.GPIOOut(Cat( platform.request("user_led", 0), platform.request("user_led", 1))) - self.comb += [ - platform.request("ttl_l_tx_en").eq(1), - platform.request("ttl_h_tx_en").eq(1) - ] - - # RTIO channels - rtio_channels = [] - for i in range(2): - phy = ttl_simple.Inout(platform.request("pmt", i)) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink, ififo_depth=512)) - for i in range(16): - phy = ttl_simple.Output(platform.request("ttl", i)) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) - - phy = ttl_simple.Output(platform.request("user_led", 2)) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) - - self.add_constant("RTIO_DDS_CHANNEL", len(rtio_channels)) - self.submodules.dds = RenameClockDomains( - ad9858.AD9858(platform.request("dds")), - "rio") - phy = RT2WB(7, self.dds.bus) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink, ififo_depth=4)) - - # RTIO core - self.submodules.rtiocrg = _RTIOCRG(platform, self.crg.pll_sys) + def add_rtio(self, rtio_channels): + self.submodules.rtio_crg = _RTIOCRG(self.platform, self.crg.pll_sys) self.submodules.rtio = rtio.RTIO(rtio_channels, clk_freq=125000000) self.add_constant("RTIO_FINE_TS_WIDTH", self.rtio.fine_ts_width) + self.add_constant("DDS_RTIO_CLK_RATIO", 8 >> self.rtio.fine_ts_width) + self.submodules.rtio_moninj = rtio.MonInj(rtio_channels) - - if isinstance(platform.toolchain, XilinxVivadoToolchain): - platform.add_platform_command(""" + if isinstance(self.platform.toolchain, XilinxVivadoToolchain): + self.platform.add_platform_command(""" create_clock -name rsys_clk -period 8.0 [get_nets {rsys_clk}] create_clock -name rio_clk -period 8.0 [get_nets {rio_clk}] set_false_path -from [get_clocks rsys_clk] -to [get_clocks rio_clk] set_false_path -from [get_clocks rio_clk] -to [get_clocks rsys_clk] """, rsys_clk=self.rtio.cd_rsys.clk, rio_clk=self.rtio.cd_rio.clk) - # CPU connections rtio_csrs = self.rtio.get_csrs() self.submodules.rtiowb = wbgen.Bank(rtio_csrs) self.kernel_cpu.add_wb_slave(mem_decoder(self.mem_map["rtio"]), @@ -107,4 +80,84 @@ set_false_path -from [get_clocks rio_clk] -to [get_clocks rsys_clk] rtio_csrs) +class NIST_QC1(_NIST_QCx): + def __init__(self, platform, cpu_type="or1k", **kwargs): + _NIST_QCx.__init__(self, platform, cpu_type, **kwargs) + platform.add_extension(nist_qc1.fmc_adapter_io) + + self.comb += [ + platform.request("ttl_l_tx_en").eq(1), + platform.request("ttl_h_tx_en").eq(1) + ] + + rtio_channels = [] + for i in range(2): + phy = ttl_simple.Inout(platform.request("pmt", i)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=512)) + for i in range(15): + phy = ttl_simple.Output(platform.request("ttl", i)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + + phy = ttl_simple.Output(platform.request("user_led", 2)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + self.add_constant("RTIO_REGULAR_TTL_COUNT", len(rtio_channels)) + + phy = ttl_simple.ClockGen(platform.request("ttl", 15)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + + self.add_constant("RTIO_DDS_CHANNEL", len(rtio_channels)) + self.add_constant("DDS_CHANNEL_COUNT", 8) + self.add_constant("DDS_AD9858") + phy = dds.AD9858(platform.request("dds"), 8) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy, + ofifo_depth=512, + ififo_depth=4)) + self.add_rtio(rtio_channels) + + +class NIST_QC2(_NIST_QCx): + def __init__(self, platform, cpu_type="or1k", **kwargs): + _NIST_QCx.__init__(self, platform, cpu_type, **kwargs) + platform.add_extension(nist_qc2.fmc_adapter_io) + + rtio_channels = [] + for i in range(16): + if i == 14: + # TTL14 is for the clock generator + continue + if i % 4 == 3: + phy = ttl_simple.Inout(platform.request("ttl", i)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=512)) + else: + phy = ttl_simple.Output(platform.request("ttl", i)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + + phy = ttl_simple.Output(platform.request("user_led", 2)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + self.add_constant("RTIO_REGULAR_TTL_COUNT", len(rtio_channels)) + + phy = ttl_simple.ClockGen(platform.request("ttl", 14)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) + + self.add_constant("RTIO_DDS_CHANNEL", len(rtio_channels)) + self.add_constant("DDS_CHANNEL_COUNT", 11) + self.add_constant("DDS_AD9914") + self.add_constant("DDS_ONEHOT_SEL") + phy = dds.AD9914(platform.request("dds"), 11) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy, + ofifo_depth=512, + ififo_depth=4)) + self.add_rtio(rtio_channels) + + default_subtarget = NIST_QC1 diff --git a/soc/targets/artiq_pipistrello.py b/soc/targets/artiq_pipistrello.py index 29259d529..fd09ed373 100644 --- a/soc/targets/artiq_pipistrello.py +++ b/soc/targets/artiq_pipistrello.py @@ -1,30 +1,32 @@ +from fractions import Fraction + from migen.fhdl.std import * from migen.bank.description import * from migen.bank import wbgen from misoclib.com import gpio from misoclib.soc import mem_decoder +from misoclib.mem.sdram.core.minicon import MiniconSettings from targets.pipistrello import BaseSoC from artiq.gateware.soc import AMPSoC -from artiq.gateware import rtio, ad9858, nist_qc1 -from artiq.gateware.rtio.phy import ttl_simple -from artiq.gateware.rtio.phy.wishbone import RT2WB +from artiq.gateware import rtio, nist_qc1 +from artiq.gateware.rtio.phy import ttl_simple, dds class _RTIOCRG(Module, AutoCSR): - def __init__(self, platform): + def __init__(self, platform, clk_freq): self._clock_sel = CSRStorage() self.clock_domains.cd_rtio = ClockDomain(reset_less=True) - # 75MHz -> 125MHz + f = Fraction(125*1000*1000, clk_freq) rtio_internal_clk = Signal() self.specials += Instance("DCM_CLKGEN", p_CLKFXDV_DIVIDE=2, - p_CLKFX_DIVIDE=3, - p_CLKFX_MD_MAX=1.6, - p_CLKFX_MULTIPLY=5, - p_CLKIN_PERIOD=1e3/75, + p_CLKFX_DIVIDE=f.denominator, + p_CLKFX_MD_MAX=float(f), + p_CLKFX_MULTIPLY=f.numerator, + p_CLKIN_PERIOD=1e9/clk_freq, p_SPREAD_SPECTRUM="NONE", p_STARTUP_WAIT="FALSE", i_CLKIN=ClockSignal(), @@ -32,8 +34,11 @@ class _RTIOCRG(Module, AutoCSR): i_FREEZEDCM=0, i_RST=ResetSignal()) - rtio_external_clk = platform.request("dds_clock") - platform.add_period_constraint(rtio_external_clk, 8.0) + rtio_external_clk = platform.request("pmt", 2) + # ISE infers constraints for the internal clock + # and propagates them through the BUFGMUX. Adding this: + # platform.add_period_constraint(rtio_external_clk, 8.0) + # seems to confuse it self.specials += Instance("BUFGMUX", i_I0=rtio_internal_clk, i_I1=rtio_external_clk, @@ -42,22 +47,18 @@ class _RTIOCRG(Module, AutoCSR): platform.add_platform_command(""" NET "{int_clk}" TNM_NET = "GRPint_clk"; -NET "{ext_clk}" TNM_NET = "GRPext_clk"; NET "sys_clk" TNM_NET = "GRPsys_clk"; TIMESPEC "TSfix_ise1" = FROM "GRPint_clk" TO "GRPsys_clk" TIG; TIMESPEC "TSfix_ise2" = FROM "GRPsys_clk" TO "GRPint_clk" TIG; -TIMESPEC "TSfix_ise3" = FROM "GRPext_clk" TO "GRPsys_clk" TIG; -TIMESPEC "TSfix_ise4" = FROM "GRPsys_clk" TO "GRPext_clk" TIG; -TIMESPEC "TSfix_ise5" = FROM "GRPext_clk" TO "GRPint_clk" TIG; -TIMESPEC "TSfix_ise6" = FROM "GRPint_clk" TO "GRPext_clk" TIG; """, int_clk=rtio_internal_clk, ext_clk=rtio_external_clk) class NIST_QC1(BaseSoC, AMPSoC): csr_map = { "rtio": None, # mapped on Wishbone instead - "rtiocrg": 13, - "kernel_cpu": 14 + "rtio_crg": 13, + "kernel_cpu": 14, + "rtio_moninj": 15 } csr_map.update(BaseSoC.csr_map) mem_map = { @@ -68,7 +69,9 @@ class NIST_QC1(BaseSoC, AMPSoC): def __init__(self, platform, cpu_type="or1k", **kwargs): BaseSoC.__init__(self, platform, - cpu_type=cpu_type, with_timer=False, **kwargs) + cpu_type=cpu_type, + sdram_controller_settings=MiniconSettings(l2_size=64*1024), + with_timer=False, **kwargs) AMPSoC.__init__(self) platform.toolchain.ise_commands += """ trce -v 12 -fastpaths -tsi {build_name}.tsi -o {build_name}.twr {build_name}.ncd {build_name}.pcf @@ -78,6 +81,8 @@ trce -v 12 -fastpaths -tsi {build_name}.tsi -o {build_name}.twr {build_name}.ncd self.submodules.leds = gpio.GPIOOut(Cat( platform.request("user_led", 0), platform.request("user_led", 1), + platform.request("user_led", 2), + platform.request("user_led", 3), )) self.comb += [ @@ -90,39 +95,44 @@ trce -v 12 -fastpaths -tsi {build_name}.tsi -o {build_name}.twr {build_name}.ncd for i in range(2): phy = ttl_simple.Inout(platform.request("pmt", i)) self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink, ififo_depth=512)) + rtio_channels.append(rtio.Channel.from_phy(phy, ififo_depth=512, + ofifo_depth=4)) - phy = ttl_simple.Inout(platform.request("xtrig", 0)) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) - - for i in range(16): + for i in range(15): phy = ttl_simple.Output(platform.request("ttl", i)) self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) + rtio_channels.append(rtio.Channel.from_phy(phy, ofifo_depth=256)) phy = ttl_simple.Output(platform.request("ext_led", 0)) self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) + rtio_channels.append(rtio.Channel.from_phy(phy, ofifo_depth=4)) - for i in range(2, 5): - phy = ttl_simple.Output(platform.request("user_led", i)) - self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink)) + phy = ttl_simple.Output(platform.request("user_led", 4)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy, ofifo_depth=4)) + + self.add_constant("RTIO_REGULAR_TTL_COUNT", len(rtio_channels)) + + phy = ttl_simple.ClockGen(platform.request("ttl", 15)) + self.submodules += phy + rtio_channels.append(rtio.Channel.from_phy(phy)) self.add_constant("RTIO_DDS_CHANNEL", len(rtio_channels)) - self.submodules.dds = RenameClockDomains( - ad9858.AD9858(platform.request("dds")), - "rio") - phy = RT2WB(7, self.dds.bus) + self.add_constant("DDS_CHANNEL_COUNT", 8) + self.add_constant("DDS_AD9858") + phy = dds.AD9858(platform.request("dds"), 8) self.submodules += phy - rtio_channels.append(rtio.Channel(phy.rtlink, ififo_depth=4)) + rtio_channels.append(rtio.Channel.from_phy(phy, + ofifo_depth=512, + ififo_depth=4)) # RTIO core - self.submodules.rtiocrg = _RTIOCRG(platform) + self.submodules.rtio_crg = _RTIOCRG(platform, self.clk_freq) self.submodules.rtio = rtio.RTIO(rtio_channels, clk_freq=125000000) self.add_constant("RTIO_FINE_TS_WIDTH", self.rtio.fine_ts_width) + self.add_constant("DDS_RTIO_CLK_RATIO", 8 >> self.rtio.fine_ts_width) + self.submodules.rtio_moninj = rtio.MonInj(rtio_channels) # CPU connections rtio_csrs = self.rtio.get_csrs()