From 74ce6adab5dd5115d3bbed5c1848a8e342f5a4d3 Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Sun, 28 Feb 2021 19:41:39 +1100 Subject: [PATCH 1/8] Initial commit of support for multi-pseudoclock PrawnBlaster. I have not yet fully implemented waits in the blacs_worker yet! --- labscript_devices/PrawnBlaster/__init__.py | 19 + labscript_devices/PrawnBlaster/blacs_tabs.py | 127 +++++++ .../PrawnBlaster/blacs_workers.py | 206 ++++++++++ .../PrawnBlaster/labscript_devices.py | 359 ++++++++++++++++++ .../PrawnBlaster/register_classes.py | 23 ++ .../PrawnBlaster/runviewer_parsers.py | 96 +++++ 6 files changed, 830 insertions(+) create mode 100644 labscript_devices/PrawnBlaster/__init__.py create mode 100644 labscript_devices/PrawnBlaster/blacs_tabs.py create mode 100644 labscript_devices/PrawnBlaster/blacs_workers.py create mode 100644 labscript_devices/PrawnBlaster/labscript_devices.py create mode 100644 labscript_devices/PrawnBlaster/register_classes.py create mode 100644 labscript_devices/PrawnBlaster/runviewer_parsers.py diff --git a/labscript_devices/PrawnBlaster/__init__.py b/labscript_devices/PrawnBlaster/__init__.py new file mode 100644 index 00000000..442af093 --- /dev/null +++ b/labscript_devices/PrawnBlaster/__init__.py @@ -0,0 +1,19 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/__init__.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +from labscript_devices import deprecated_import_alias + + +# For backwards compatibility with old experiment scripts: +PrawnBlaster = deprecated_import_alias( + "labscript_devices.PrawnBlaster.labscript_devices.PrawnBlaster" +) diff --git a/labscript_devices/PrawnBlaster/blacs_tabs.py b/labscript_devices/PrawnBlaster/blacs_tabs.py new file mode 100644 index 00000000..25ab73d0 --- /dev/null +++ b/labscript_devices/PrawnBlaster/blacs_tabs.py @@ -0,0 +1,127 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/blacs_tab.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +from blacs.device_base_class import ( + DeviceTab, + define_state, + MODE_BUFFERED, + MODE_MANUAL, + MODE_TRANSITION_TO_BUFFERED, + MODE_TRANSITION_TO_MANUAL, +) +import labscript_utils.properties + +from qtutils.qt import QtWidgets + + +class PrawnBlasterTab(DeviceTab): + def initialise_GUI(self): + self.connection_table_properties = ( + self.settings["connection_table"].find_by_name(self.device_name).properties + ) + + digital_outs = {} + for pin in self.connection_table_properties["out_pins"]: + digital_outs[f"GPIO {pin:02d}"] = {} + + # Create a single digital output + self.create_digital_outputs(digital_outs) + # Create widgets for output objects + _, _, do_widgets = self.auto_create_widgets() + # and auto place the widgets in the UI + self.auto_place_widgets(("Flags", do_widgets)) + + # Create status labels + self.status_label = QtWidgets.QLabel("Status: Unknown") + self.clock_status_label = QtWidgets.QLabel("Clock status: Unknown") + self.get_tab_layout().addWidget(self.status_label) + self.get_tab_layout().addWidget(self.clock_status_label) + + # Set the capabilities of this device + self.supports_smart_programming(True) + + # Create status monitor timout + self.statemachine_timeout_add(2000, self.status_monitor) + + def get_child_from_connection_table(self, parent_device_name, port): + # Pass down channel name search to the pseudoclocks (so we can find the + # clocklines) + if parent_device_name == self.device_name: + device = self.connection_table.find_by_name(self.device_name) + + for pseudoclock_name, pseudoclock in device.child_list.items(): + for child_name, child in pseudoclock.child_list.items(): + # store a reference to the internal clockline + if child.parent_port == port: + return DeviceTab.get_child_from_connection_table( + self, pseudoclock.name, port + ) + + return None + + def initialise_workers(self): + # Find the COM port to be used + com_port = str( + self.settings["connection_table"] + .find_by_name(self.device_name) + .BLACS_connection + ) + + worker_initialisation_kwargs = { + "com_port": com_port, + "num_pseudoclocks": self.connection_table_properties["num_pseudoclocks"], + "out_pins": self.connection_table_properties["out_pins"], + "in_pins": self.connection_table_properties["in_pins"], + } + self.create_worker( + "main_worker", + "labscript_devices.PrawnBlaster.blacs_workers.PrawnBlasterWorker", + worker_initialisation_kwargs, + ) + self.primary_worker = "main_worker" + + @define_state( + MODE_MANUAL + | MODE_BUFFERED + | MODE_TRANSITION_TO_BUFFERED + | MODE_TRANSITION_TO_MANUAL, + True, + ) + def status_monitor(self, notify_queue=None): + # When called with a queue, this function writes to the queue + # when the pulseblaster is waiting. This indicates the end of + # an experimental run. + status, clock_status, waits_pending = yield ( + self.queue_work(self.primary_worker, "check_status") + ) + + # Manual mode or aborted + done_condition = status == 0 or status == 5 + + # Update GUI status/clock status widgets + self.status_label.setText(f"Status: {status}") + self.clock_status_label.setText(f"Clock status: {clock_status}") + + if notify_queue is not None and done_condition and not waits_pending: + # Experiment is over. Tell the queue manager about it, then + # set the status checking timeout back to every 2 seconds + # with no queue. + notify_queue.put("done") + self.statemachine_timeout_remove(self.status_monitor) + self.statemachine_timeout_add(2000, self.status_monitor) + + @define_state(MODE_BUFFERED, True) + def start_run(self, notify_queue): + self.statemachine_timeout_remove(self.status_monitor) + yield (self.queue_work(self.primary_worker, "start_run")) + self.status_monitor() + self.statemachine_timeout_add(100, self.status_monitor, notify_queue) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py new file mode 100644 index 00000000..e69b15ac --- /dev/null +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -0,0 +1,206 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/blacs_worker.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +import time +import labscript_utils.h5_lock +import h5py +from blacs.tab_base_classes import Worker +import labscript_utils.properties as properties + + +class PrawnBlasterWorker(Worker): + def init(self): + # fmt: off + global h5py; import labscript_utils.h5_lock, h5py + global serial; import serial + global time; import time + global re; import re + self.smart_cache = {} + self.cached_pll_params = {} + # fmt: on + + self.prawnblaster = serial.Serial(self.com_port, 115200, timeout=1) + self.check_status() + + # configure number of pseudoclocks + self.prawnblaster.write(b"setnumpseudoclocks %d\r\n" % self.num_pseudoclocks) + assert self.prawnblaster.readline().decode() == "ok\r\n" + + # Configure pins + for i, (out_pin, in_pin) in enumerate(zip(self.out_pins, self.in_pins)): + self.prawnblaster.write(b"setoutpin %d %d\r\n" % (i, out_pin)) + assert self.prawnblaster.readline().decode() == "ok\r\n" + self.prawnblaster.write(b"setinpin %d %d\r\n" % (i, in_pin)) + assert self.prawnblaster.readline().decode() == "ok\r\n" + + def check_status(self): + self.prawnblaster.write(b"status\r\n") + response = self.prawnblaster.readline().decode() + match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response) + if match: + return int(match.group(1)), int(match.group(2)), False + elif response: + raise Exception( + f"PrawnBlaster is confused: saying '{response}' instead of 'run-status: clock-status:'" + ) + else: + raise Exception( + f"PrawnBlaster is returning a invalid status '{response}'. Maybe it needs a reboot." + ) + + def program_manual(self, values): + for channel, value in values.items(): + pin = int(channel.split()[1]) + pseudoclock = self.out_pins.index(pin) + if value: + self.prawnblaster.write(b"go high %d\r\n" % pseudoclock) + else: + self.prawnblaster.write(b"go low %d\r\n" % pseudoclock) + + assert self.prawnblaster.readline().decode() == "ok\r\n" + + return values + + def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + if fresh: + self.smart_cache = {} + + # Get data from HDF5 file + pulse_programs = [] + with h5py.File(h5file, "r") as hdf5_file: + group = hdf5_file[f"devices/{device_name}"] + for i in range(self.num_pseudoclocks): + pulse_programs.append(group[f"PULSE_PROGRAM_{i}"][:]) + self.smart_cache.setdefault(i, []) + device_properties = labscript_utils.properties.get( + hdf5_file, device_name, "device_properties" + ) + self.is_master_pseudoclock = device_properties["is_master_pseudoclock"] + + # TODO: Configure clock from device properties + clock_mode = 0 + clock_vcofreq = 0 + clock_plldiv1 = 0 + clock_plldiv2 = 0 + if device_properties["external_clock_pin"] is not None: + if device_properties["external_clock_pin"] == 20: + clock_mode = 1 + elif device_properties["external_clock_pin"] == 22: + clock_mode = 2 + else: + raise RuntimeError( + f"Invalid external clock pin {device_properties['external_clock_pin']}. Pin must be 20, 22 or None." + ) + clock_frequency = device_properties["clock_frequency"] + + if clock_mode == 0: + if clock_frequency == 100e6: + clock_vcofreq = 1200e6 + clock_plldiv1 = 6 + clock_plldiv2 = 2 + elif clock_frequency in self.cached_pll_params: + pll_params = self.cached_pll_params[clock_frequency] + clock_vcofreq = pll_params["vcofreq"] + clock_plldiv1 = pll_params["plldiv1"] + clock_plldiv2 = pll_params["plldiv2"] + else: + self.logger.info("Calculating PLL parameters...") + osc_freq = 12e6 + # Techniclally FBDIV can be 16-320 (see 2.18.2 in + # https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf ) + # however for a 12MHz reference clock, the range is smaller to ensure + # vcofreq is between 400 and 1600 MHz. + found = False + for fbdiv in range(134, 33, -1): + vcofreq = osc_freq * fbdiv + # PLL1 div should be greater than pll2 div if possible so we start high + for pll1 in range(7, 0, -1): + for pll2 in range(1, 8): + if vco_freq / (pll1 * pll2) == clock_frequency: + found = True + clock_vcofreq = vcofreq + clock_plldiv1 = pll1 + clock_plldiv2 = pll2 + pll_params = {} + pll_params["vcofreq"] = clock_vcofreq + pll_params["plldiv1"] = clock_plldiv1 + pll_params["plldiv2"] = clock_plldiv2 + self.cached_pll_params[clock_frequency] = pll_params + break + if found: + break + if found: + break + if not found: + raise RuntimeError( + "Could not determine appropriate clock paramaters" + ) + + # Now set the clock details + self.prawnblaster.write( + b"setclock %d %d %d %d %d\r\n" + % (clock_mode, clock_frequency, clock_vcofreq, clock_plldiv1, clock_plldiv2) + ) + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + # TODO: Save any information we need for wait monitor + + # Program instructions + for pseudoclock, pulse_program in enumerate(pulse_programs): + for i, instruction in enumerate(pulse_program): + if i == len(self.smart_cache[pseudoclock]): + # Pad the smart cache out to be as long as the program: + self.smart_cache[pseudoclock].append(None) + + # Only program instructions that differ from what's in the smart cache: + if self.smart_cache[pseudoclock][i] != instruction: + self.prawnblaster.write( + b"set %d %d %d %d\r\n" + % (pseudoclock, i, instruction["period"], instruction["reps"]) + ) + response = self.prawnblaster.readline().decode() + assert ( + response == "ok\r\n" + ), f"PrawnBlaster said '{response}', expected 'ok'" + self.smart_cache[pseudoclock][i] = instruction + + # All outputs end on 0 + final = {} + for pin in self.out_pins: + final[f"GPIO {pin:02d}"] = 0 + return final + + def start_run(self): + # Start in software: + self.logger.info("sending start") + self.prawnblaster.write(b"start\r\n") + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + def transition_to_manual(self): + # TODO: write this + return True + + def shutdown(self): + self.prawnblaster.close() + + def abort_buffered(self): + self.prawnblaster.write(b"abort\r\n") + assert self.prawnblaster.readline().decode() == "ok\r\n" + # loop until abort complete + while self.check_status()[0] != 5: + time.sleep(0.5) + return True + + def abort_transition_to_buffered(self): + return self.abort_buffered() diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py new file mode 100644 index 00000000..f5f52197 --- /dev/null +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -0,0 +1,359 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/labscript_devices.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +import copy + +from labscript import ( + ClockLine, + IntermediateDevice, + LabscriptError, + PseudoclockDevice, + Pseudoclock, + WaitMonitor, + compiler, + config, + set_passed_properties, +) +import numpy as np + + +class _PrawnBlasterPseudoclock(Pseudoclock): + def __init__(self, i, *args, **kwargs): + super().__init__(*args, **kwargs) + self.i = i + + def add_device(self, device): + if isinstance(device, ClockLine): + # only allow one child + if self.child_devices: + raise LabscriptError( + f"Each pseudoclock of the PrawnBlaster {self.parent_device.name} only supports 1 clockline, which is automatically created. Please use the clockline located at {self.parent_device.name}.clockline[{self.i}]" + ) + Pseudoclock.add_device(self, device) + else: + raise LabscriptError( + f"You have connected {device.name} to {self.name} (a Pseudoclock of {self.parent_device.name}), but {self.name} only supports children that are ClockLines. Please connect your device to {self.parent_device.name}.clockline[{self.i}] instead." + ) + + +# +# Define dummy pseudoclock/clockline/intermediatedevice to trick wait monitor +# since everything is handled internally in this device +# +class _PrawnBlasterDummyPseudoclock(Pseudoclock): + def add_device(self, device): + if isinstance(device, _PrawnBlasterDummyClockLine): + if self.child_devices: + raise LabscriptError( + f"You are trying to access the special, dummy, PseudoClock of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + Pseudoclock.add_device(self, device) + else: + raise LabscriptError( + f"You are trying to access the special, dummy, PseudoClock of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class _PrawnBlasterDummyClockLine(ClockLine): + def add_device(self, device): + if isinstance(device, _PrawnBlasterDummyIntermediateDevice): + if self.child_devices: + raise LabscriptError( + f"You are trying to access the special, dummy, ClockLine of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + ClockLine.add_device(self, device) + else: + raise LabscriptError( + f"You are trying to access the special, dummy, ClockLine of the PrawnBlaster {self.pseudoclock_device.name}. This is for internal use only." + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class _PrawnBlasterDummyIntermediateDevice(IntermediateDevice): + def add_device(self, device): + if isinstance(device, WaitMonitor): + IntermediateDevice.add_device(self, device) + else: + raise LabscriptError( + "You can only connect an instance of WaitMonitor to the device %s.internal_wait_monitor_outputs" + % (self.pseudoclock_device.name) + ) + + # do nothing, this is a dummy class! + def generate_code(self, *args, **kwargs): + pass + + +class PrawnBlaster(PseudoclockDevice): + description = "PrawnBlaster" + clock_limit = 1 / 60e-9 + clock_resolution = 10e-9 + trigger_delay = 70e-9 + wait_delay = 40e-9 + allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock] + max_instructions = 60000 + + @set_passed_properties( + property_names={ + "connection_table_properties": [ + "com_port", + "in_pins", + "out_pins", + "num_pseudoclocks", + ], + "device_properties": [ + "clock_frequency", + "external_clock_pin", + "clock_limit", + "clock_resolution", + "trigger_delay", + "wait_delay", + "max_instructions", + ], + } + ) + def __init__( + self, + name, + trigger_device=None, + trigger_connection=None, + com_port="COM1", + num_pseudoclocks=1, + out_pins=None, + in_pins=None, + clock_frequency=100e6, + external_clock_pin=None, + use_wait_monitor=True, + ): + # Check number of pseudoclocks is within range + if num_pseudoclocks < 1 or num_pseudoclocks > 4: + raise LabscriptError( + f"The PrawnBlaster {name} only supports between 1 and 4 pseudoclocks" + ) + + # Update the specs based on the number of pseudoclocks + self.max_instructions = self.max_instructions // num_pseudoclocks + # Update the specs based on the clock frequency + if self.clock_resolution != 1 / clock_frequency: + factor = (1 / clock_frequency) / self.clock_resolution + self.clock_limit *= factor + self.clock_resolution *= factor + self.trigger_delay *= factor + self.wait_delay *= factor + + # Instantiate the base class + PseudoclockDevice.__init__(self, name, trigger_device, trigger_connection) + self.num_pseudoclocks = num_pseudoclocks + # Wait monitor can only be used if this is the master pseudoclock + self.use_wait_monitor = use_wait_monitor and self.is_master_pseudoclock + + # Set the BLACS connections + self.BLACS_connection = com_port + + # Check in/out pins + if out_pins is None: + out_pins = [9, 11, 13, 15] + if in_pins is None: + in_pins = [0, 2, 4, 6] + if len(out_pins) < num_pseudoclocks: + raise LabscriptError( + f"The PrawnBlaster {self.name} is configured with {num_pseudoclocks} but only has pin numbers specified for {len(out_pins)}." + ) + else: + self.out_pins = out_pins[:num_pseudoclocks] + if len(in_pins) < num_pseudoclocks: + raise LabscriptError( + f"The PrawnBlaster {self.name} is configured with {num_pseudoclocks} but only has pin numbers specified for {len(in_pins)}." + ) + else: + self.in_pins = in_pins[:num_pseudoclocks] + + self._pseudoclocks = [] + self._clocklines = [] + for i in range(num_pseudoclocks): + self._pseudoclocks.append( + _PrawnBlasterPseudoclock( + i, + name=f"{name}_pseudoclock_{i}", + pseudoclock_device=self, + connection=f"pseudoclock {i}", + ) + ) + self._clocklines.append( + ClockLine( + name=f"{name}_clock_line_{i}", + pseudoclock=self._pseudoclocks[i], + connection=f"GPIO {self.out_pins[i]}", + ) + ) + + if self.use_wait_monitor: + # Create internal devices for connecting to a wait monitor + self.__wait_monitor_dummy_pseudoclock = _PrawnBlasterDummyPseudoclock( + "%s__dummy_wait_pseudoclock" % name, self, "_" + ) + self.__wait_monitor_dummy_clock_line = _PrawnBlasterDummyClockLine( + "%s__dummy_wait_clock_line" % name, + self.__wait_monitor_dummy_pseudoclock, + "_", + ) + self.__wait_monitor_intermediate_device = ( + _PrawnBlasterDummyIntermediateDevice( + "%s_internal_wait_monitor_outputs" % name, + self.__wait_monitor_dummy_clock_line, + ) + ) + + # Create the wait monitor + WaitMonitor( + "%s__wait_monitor" % name, + self.internal_wait_monitor_outputs, + "internal", + self.internal_wait_monitor_outputs, + "internal", + self.internal_wait_monitor_outputs, + "internal", + ) + + @property + def internal_wait_monitor_outputs(self): + return self.__wait_monitor_intermediate_device + + @property + def pseudoclocks(self): + return copy.copy(self._pseudoclocks) + + @property + def clocklines(self): + return copy.copy(self._clocklines) + + def add_device(self, device): + if len(self.child_devices) < ( + self.num_pseudoclocks + self.use_wait_monitor + ) and isinstance(device, (_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock)): + PseudoclockDevice.add_device(self, device) + elif isinstance(device, _PrawnBlasterPseudoclock): + raise LabscriptError( + f"The {self.description} {self.name} automatically creates the correct number of pseudoclocks." + + "Instead of instantiating your own Pseudoclock object, please use the internal" + + f" ones stored in {self.name}.pseudoclocks" + ) + else: + raise LabscriptError( + f"You have connected {device.name} (class {device.__class__}) to {self.name}, but {self.name} does not support children with that class." + ) + + def generate_code(self, hdf5_file): + PseudoclockDevice.generate_code(self, hdf5_file) + group = self.init_device_group(hdf5_file) + + current_wait_index = 0 + wait_table = sorted(compiler.wait_table) + + # For each pseudoclock + for i, pseudoclock in enumerate(self.pseudoclocks): + current_wait_index = 0 + + # Compress clock instructions with the same period + reduced_instructions = [] + for instruction in pseudoclock.clock: + if instruction == "WAIT": + # If we're using the internal wait monitor, set the timeout + if self.use_wait_monitor: + # Get the wait timeout value + wait_timeout = compiler.wait_table[ + wait_table[current_wait_index] + ][1] + current_wait_index += 1 + # The following period and reps indicates a wait instruction + reduced_instructions.append( + { + "period": round(wait_timeout / self.clock_resolution), + "reps": 0, + } + ) + continue + # Else, set an indefinite wait and wait for a trigger from something else. + else: + # Two waits in a row are an indefinite wait + reduced_instructions.append( + { + "period": 2 ** 32 - 1, + "reps": 0, + } + ) + reduced_instructions.append( + { + "period": 2 ** 32 - 1, + "reps": 0, + } + ) + + # Normal instruction + reps = instruction["reps"] + # period is in quantised units: + period = int(round(instruction["step"] / self.clock_resolution)) + if ( + # If there is a previous instruction + reduced_instructions + # And it's not a wait + and reduced_instructions[-1]["reps"] != 0 + # And the periods match + and reduced_instructions[-1]["period"] == period + # And the sum of the previous reps and current reps won't push it over the limit + and (reduced_instructions[-1]["reps"] + reps) < (2 ** 32 - 1) + ): + # Combine instructions! + reduced_instructions[-1]["reps"] += reps + else: + # New instruction + reduced_instructions.append({"period": period, "reps": reps}) + + # Only add this if there is room in the instruction table. The PrawnBlaster + # firmware has extre room at the end for an instruction that is always 0 + # and cannot be set over serial! + if len(reduced_instructions) != self.max_instructions: + # The following period and reps indicates a stop instruction: + reduced_instructions.append({"period": 0, "reps": 0}) + + # Check we have not exceeded the maximum number of supported instructions + # for this number of speudoclocks + if len(reduced_instructions) > self.max_instructions: + raise LabscriptError( + f"{self.description} {self.name}.clocklines[{i}] has too many instructions. It has {len(reduced_instructions)} and can only support {self.max_instructions}" + ) + + # Store these instructions to the h5 file: + dtypes = [("period", int), ("reps", int)] + pulse_program = np.zeros(len(reduced_instructions), dtype=dtypes) + for j, instruction in enumerate(reduced_instructions): + pulse_program[j]["period"] = instruction["period"] + pulse_program[j]["reps"] = instruction["reps"] + group.create_dataset( + f"PULSE_PROGRAM_{i}", compression=config.compression, data=pulse_program + ) + + # TODO: is this needed, the PulseBlasters don't save it... + self.set_property( + "is_master_pseudoclock", + self.is_master_pseudoclock, + location="device_properties", + ) + self.set_property("stop_time", self.stop_time, location="device_properties") diff --git a/labscript_devices/PrawnBlaster/register_classes.py b/labscript_devices/PrawnBlaster/register_classes.py new file mode 100644 index 00000000..f94be1ad --- /dev/null +++ b/labscript_devices/PrawnBlaster/register_classes.py @@ -0,0 +1,23 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/register_classes.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### +import labscript_devices + +labscript_device_name = 'PrawnBlaster' +blacs_tab = 'labscript_devices.PrawnBlaster.blacs_tabs.PrawnBlasterTab' +parser = 'labscript_devices.PrawnBlaster.runviewer_parsers.PrawnBlasterParser' + +labscript_devices.register_classes( + labscript_device_name=labscript_device_name, + BLACS_tab=blacs_tab, + runviewer_parser=parser, +) diff --git a/labscript_devices/PrawnBlaster/runviewer_parsers.py b/labscript_devices/PrawnBlaster/runviewer_parsers.py new file mode 100644 index 00000000..f390b6c7 --- /dev/null +++ b/labscript_devices/PrawnBlaster/runviewer_parsers.py @@ -0,0 +1,96 @@ +##################################################################### +# # +# /labscript_devices/PrawnBlaster/runviewer_parsers.py # +# # +# Copyright 2021, Philip Starkey # +# # +# This file is part of labscript_devices, in the labscript suite # +# (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +import labscript_utils.h5_lock # noqa: F401 +import h5py +import numpy as np + +import labscript_utils.properties as properties + + +class PrawnBlasterParser(object): + def __init__(self, path, device): + self.path = path + self.name = device.name + self.device = device + + def get_traces(self, add_trace, clock=None): + if clock is not None: + times, clock_value = clock[0], clock[1] + clock_indices = np.where((clock_value[1:] - clock_value[:-1]) == 1)[0] + 1 + # If initial clock value is 1, then this counts as a rising edge + # (clock should be 0 before experiment) but this is not picked up + # by the above code. So we insert it! + if clock_value[0] == 1: + clock_indices = np.insert(clock_indices, 0, 0) + clock_ticks = times[clock_indices] + + # get the pulse program + pulse_programs = [] + with h5py.File(self.path, 'r') as f: + # Get the device properties + device_props = properties.get(f, self.name, 'device_properties') + conn_props = properties.get(f, self.name, 'connection_table_properties') + + self.clock_resolution = device_props["clock_resolution"] + self.trigger_delay = device_props["trigger_delay"] + self.wait_delay = device_props["wait_delay"] + + # Extract the pulse programs + num_pseudoclocks = conn_props["num_pseudoclocks"] + for i in range(num_pseudoclocks): + pulse_programs.append(f[f'devices/{self.name}/PULSE_PROGRAM_{i}'][:]) + + # Generate clocklines and triggers + clocklines_and_triggers = {} + for pulse_program in pulse_programs: + time = [] + states = [] + trigger_index = 0 + t = 0 if clock is None else clock_ticks[trigger_index] + self.trigger_delay + trigger_index += 1 + + clock_factor = self.clock_resolution / 2.0 + + last_instruction_was_wait = False + for row in pulse_program: + if row['reps'] == 0 and not last_instruction_was_wait: # WAIT + last_instruction_was_wait = True + if clock is not None: + t = clock_ticks[trigger_index] + self.trigger_delay + trigger_index += 1 + else: + t += self.wait_delay + elif last_instruction_was_wait: + # two waits in a row means an indefinite wait, so we just skip this + # instruction. + last_instruction_was_wait = False + continue + else: + last_instruction_was_wait = False + for i in range(row['reps']): + for j in range(1, -1, -1): + time.append(t) + states.append(j) + t += row['period'] * clock_factor + + clock = (np.array(time), np.array(states)) + + for pseudoclock_name, pseudoclock in self.device.child_list.items(): + for clock_line_name, clock_line in pseudoclock.child_list.items(): + # Ignore the dummy internal wait monitor clockline + if clock_line.parent_port.startswith("GPIO"): + clocklines_and_triggers[clock_line_name] = clock + add_trace(clock_line_name, clock, self.name, clock_line.parent_port) + + return clocklines_and_triggers From b83175244f7b42efcb05c8b7696ee11b9d1af48b Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 5 Apr 2021 00:22:20 +1000 Subject: [PATCH 2/8] Updated PrawnBlaster device classes to support wait monitor and also to have all trigger pins share GPIO0 by default (since PseudoclockDevices can only have a single trigger in labscript regardless of the number of pseudoclocks) --- .../PrawnBlaster/blacs_workers.py | 190 ++++++++++++------ .../PrawnBlaster/labscript_devices.py | 4 +- 2 files changed, 130 insertions(+), 64 deletions(-) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index e69b15ac..8d2fc8f3 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -14,6 +14,7 @@ import labscript_utils.h5_lock import h5py from blacs.tab_base_classes import Worker +from labscript_utils.connections import _ensure_str import labscript_utils.properties as properties @@ -24,10 +25,24 @@ def init(self): global serial; import serial global time; import time global re; import re + global numpy; import numpy + global zprocess; import zprocess self.smart_cache = {} self.cached_pll_params = {} # fmt: on + self.all_waits_finished = zprocess.Event("all_waits_finished", type="post") + self.wait_durations_analysed = zprocess.Event( + "wait_durations_analysed", type="post" + ) + self.wait_completed = zprocess.Event("wait_completed", type="post") + self.current_wait = 0 + self.wait_table = None + self.measured_waits = None + self.wait_timeout = None + self.h5_file = None + self.started = False + self.prawnblaster = serial.Serial(self.com_port, 115200, timeout=1) self.check_status() @@ -43,11 +58,64 @@ def init(self): assert self.prawnblaster.readline().decode() == "ok\r\n" def check_status(self): + if self.started and self.wait_table is not None and self.current_wait < len(self.wait_table): + # Try to read out wait. For now, we're only reading out waits from + # pseudoclock 0 since they should all be the same (requirement imposed by labscript) + self.prawnblaster.write(b"getwait %d %d\r\n" % (0, self.current_wait)) + response = self.prawnblaster.readline().decode() + if response != "wait not yet available\r\n": + # Parse the response from the PrawnBlaster + wait_remaining = int(response) + clock_resolution = self.device_properties["clock_resolution"] + timeout_length = round( + self.wait_table[self.current_wait]["timeout"] / clock_resolution + ) + + if wait_remaining == (2 ** 32 - 1): + # The wait hit the timeout - save the timeout duration as wait length + # and flag that this wait timedout + self.measured_waits[self.current_wait] = ( + timeout_length * clock_resolution + ) + self.wait_timeout[self.current_wait] = True + else: + # Calculate wait length + self.measured_waits[self.current_wait] = ( + timeout_length - wait_remaining + ) * clock_resolution + self.wait_timeout[self.current_wait] = False + + self.logger.info( + f"Wait {self.current_wait} finished. Length={self.measured_waits[self.current_wait]:.9f}s. Timed-out={self.wait_timeout[self.current_wait]}" + ) + + # Inform any interested parties that a wait has completed: + self.wait_completed.post( + self.h5_file, + data=_ensure_str(self.wait_table[self.current_wait]["label"]), + ) + + # increment the wait we are looking for! + self.current_wait += 1 + + # post message if all waits are done + if len(self.wait_table) == self.current_wait: + self.logger.info("All waits finished") + self.all_waits_finished.post(self.h5_file) + + # Determine if we are still waiting for wait information + waits_pending = False + if self.wait_table is not None: + if self.current_wait == len(self.wait_table): + waits_pending = False + else: + waits_pending = True + self.prawnblaster.write(b"status\r\n") response = self.prawnblaster.readline().decode() match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response) if match: - return int(match.group(1)), int(match.group(2)), False + return int(match.group(1)), int(match.group(2)), waits_pending elif response: raise Exception( f"PrawnBlaster is confused: saying '{response}' instead of 'run-status: clock-status:'" @@ -74,6 +142,11 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if fresh: self.smart_cache = {} + self.h5_file = h5file # store reference to h5 file for wait monitor + self.current_wait = 0 # reset wait analysis + self.started = False # Prevent status check from detecting previous wait values + # betwen now and when we actually send the start signal + # Get data from HDF5 file pulse_programs = [] with h5py.File(h5file, "r") as hdf5_file: @@ -81,80 +154,49 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): for i in range(self.num_pseudoclocks): pulse_programs.append(group[f"PULSE_PROGRAM_{i}"][:]) self.smart_cache.setdefault(i, []) - device_properties = labscript_utils.properties.get( + self.device_properties = labscript_utils.properties.get( hdf5_file, device_name, "device_properties" ) - self.is_master_pseudoclock = device_properties["is_master_pseudoclock"] + self.is_master_pseudoclock = self.device_properties["is_master_pseudoclock"] - # TODO: Configure clock from device properties + # waits + dataset = hdf5_file["waits"] + acquisition_device = dataset.attrs["wait_monitor_acquisition_device"] + timeout_device = dataset.attrs["wait_monitor_timeout_device"] + if ( + len(dataset) > 0 + and acquisition_device + == "%s_internal_wait_monitor_outputs" % device_name + and timeout_device == "%s_internal_wait_monitor_outputs" % device_name + ): + self.wait_table = dataset[:] + self.measured_waits = numpy.zeros(len(self.wait_table)) + self.wait_timeout = numpy.zeros(len(self.wait_table), dtype=bool) + else: + self.wait_table = ( + None # This device doesn't need to worry about looking at waits + ) + self.measured_waits = None + self.wait_timeout = None + + # Configure clock from device properties clock_mode = 0 - clock_vcofreq = 0 - clock_plldiv1 = 0 - clock_plldiv2 = 0 - if device_properties["external_clock_pin"] is not None: - if device_properties["external_clock_pin"] == 20: + if self.device_properties["external_clock_pin"] is not None: + if self.device_properties["external_clock_pin"] == 20: clock_mode = 1 - elif device_properties["external_clock_pin"] == 22: + elif self.device_properties["external_clock_pin"] == 22: clock_mode = 2 else: raise RuntimeError( - f"Invalid external clock pin {device_properties['external_clock_pin']}. Pin must be 20, 22 or None." + f"Invalid external clock pin {self.device_properties['external_clock_pin']}. Pin must be 20, 22 or None." ) - clock_frequency = device_properties["clock_frequency"] - - if clock_mode == 0: - if clock_frequency == 100e6: - clock_vcofreq = 1200e6 - clock_plldiv1 = 6 - clock_plldiv2 = 2 - elif clock_frequency in self.cached_pll_params: - pll_params = self.cached_pll_params[clock_frequency] - clock_vcofreq = pll_params["vcofreq"] - clock_plldiv1 = pll_params["plldiv1"] - clock_plldiv2 = pll_params["plldiv2"] - else: - self.logger.info("Calculating PLL parameters...") - osc_freq = 12e6 - # Techniclally FBDIV can be 16-320 (see 2.18.2 in - # https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf ) - # however for a 12MHz reference clock, the range is smaller to ensure - # vcofreq is between 400 and 1600 MHz. - found = False - for fbdiv in range(134, 33, -1): - vcofreq = osc_freq * fbdiv - # PLL1 div should be greater than pll2 div if possible so we start high - for pll1 in range(7, 0, -1): - for pll2 in range(1, 8): - if vco_freq / (pll1 * pll2) == clock_frequency: - found = True - clock_vcofreq = vcofreq - clock_plldiv1 = pll1 - clock_plldiv2 = pll2 - pll_params = {} - pll_params["vcofreq"] = clock_vcofreq - pll_params["plldiv1"] = clock_plldiv1 - pll_params["plldiv2"] = clock_plldiv2 - self.cached_pll_params[clock_frequency] = pll_params - break - if found: - break - if found: - break - if not found: - raise RuntimeError( - "Could not determine appropriate clock paramaters" - ) + clock_frequency = self.device_properties["clock_frequency"] # Now set the clock details - self.prawnblaster.write( - b"setclock %d %d %d %d %d\r\n" - % (clock_mode, clock_frequency, clock_vcofreq, clock_plldiv1, clock_plldiv2) - ) + self.prawnblaster.write(b"setclock %d %d\r\n" % (clock_mode, clock_frequency)) response = self.prawnblaster.readline().decode() assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" - # TODO: Save any information we need for wait monitor - # Program instructions for pseudoclock, pulse_program in enumerate(pulse_programs): for i, instruction in enumerate(pulse_program): @@ -187,8 +229,32 @@ def start_run(self): response = self.prawnblaster.readline().decode() assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + # set started = True + self.started = True + def transition_to_manual(self): - # TODO: write this + if self.wait_table is not None: + with h5py.File(self.h5_file, "a") as hdf5_file: + # Work out how long the waits were, save em, post an event saying so + dtypes = [ + ("label", "a256"), + ("time", float), + ("timeout", float), + ("duration", float), + ("timed_out", bool), + ] + data = numpy.empty(len(self.wait_table), dtype=dtypes) + data["label"] = self.wait_table["label"] + data["time"] = self.wait_table["time"] + data["timeout"] = self.wait_table["timeout"] + data["duration"] = self.measured_waits + data["timed_out"] = self.wait_timeout + + self.logger.info(str(data)) + + hdf5_file.create_dataset("/data/waits", data=data) + + self.wait_durations_analysed.post(self.h5_file) return True def shutdown(self): diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index f5f52197..b4fdb37b 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -171,7 +171,7 @@ def __init__( if out_pins is None: out_pins = [9, 11, 13, 15] if in_pins is None: - in_pins = [0, 2, 4, 6] + in_pins = [0, 0, 0, 0] if len(out_pins) < num_pseudoclocks: raise LabscriptError( f"The PrawnBlaster {self.name} is configured with {num_pseudoclocks} but only has pin numbers specified for {len(out_pins)}." @@ -350,7 +350,7 @@ def generate_code(self, hdf5_file): f"PULSE_PROGRAM_{i}", compression=config.compression, data=pulse_program ) - # TODO: is this needed, the PulseBlasters don't save it... + # This is needed so the BLACS worker knows whether or not to be a wait monitor self.set_property( "is_master_pseudoclock", self.is_master_pseudoclock, From 2a67e79b7c6236016aaaffae2ab5293e24916336 Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 5 Apr 2021 13:39:04 +1000 Subject: [PATCH 3/8] Updated timing specs in labscript class --- labscript_devices/PrawnBlaster/labscript_devices.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index b4fdb37b..0abcb601 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -105,7 +105,13 @@ class PrawnBlaster(PseudoclockDevice): description = "PrawnBlaster" clock_limit = 1 / 60e-9 clock_resolution = 10e-9 - trigger_delay = 70e-9 + # There appears to be ~50ns buffer on input and then we know there is 80ns between + # trigger detection and first output pulse + trigger_delay = 130e-9 + # Overestimate that covers indefinite waits (which labscript does not yet support) + trigger_minimum_duration = 160e-9 + # There are 4 ASM instructions between end of pulse and being ready to detect + # a retrigger wait_delay = 40e-9 allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock] max_instructions = 60000 @@ -124,6 +130,7 @@ class PrawnBlaster(PseudoclockDevice): "clock_limit", "clock_resolution", "trigger_delay", + "trigger_minimum_duration", "wait_delay", "max_instructions", ], @@ -156,6 +163,7 @@ def __init__( self.clock_limit *= factor self.clock_resolution *= factor self.trigger_delay *= factor + self.trigger_minimum_duration *= factor self.wait_delay *= factor # Instantiate the base class From d4e68c3f4baf8e8dd1c751244288f3c6a9c7df0c Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Mon, 5 Apr 2021 13:50:32 +1000 Subject: [PATCH 4/8] More tweaks to wait length calculation --- .../PrawnBlaster/blacs_workers.py | 19 +++++++++++++++---- .../PrawnBlaster/labscript_devices.py | 5 ++++- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index 8d2fc8f3..f7895a82 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -58,7 +58,11 @@ def init(self): assert self.prawnblaster.readline().decode() == "ok\r\n" def check_status(self): - if self.started and self.wait_table is not None and self.current_wait < len(self.wait_table): + if ( + self.started + and self.wait_table is not None + and self.current_wait < len(self.wait_table) + ): # Try to read out wait. For now, we're only reading out waits from # pseudoclock 0 since they should all be the same (requirement imposed by labscript) self.prawnblaster.write(b"getwait %d %d\r\n" % (0, self.current_wait)) @@ -67,6 +71,7 @@ def check_status(self): # Parse the response from the PrawnBlaster wait_remaining = int(response) clock_resolution = self.device_properties["clock_resolution"] + input_response_time = self.device_properties["input_response_time"] timeout_length = round( self.wait_table[self.current_wait]["timeout"] / clock_resolution ) @@ -80,9 +85,13 @@ def check_status(self): self.wait_timeout[self.current_wait] = True else: # Calculate wait length + # This is a measurement of between the end of the last pulse and the + # retrigger signal. We obtain this by subtracting off the time it takes + # to detect the pulse in the ASM code once the trigger has hit the input + # pin (stored in input_response_time) self.measured_waits[self.current_wait] = ( - timeout_length - wait_remaining - ) * clock_resolution + (timeout_length - wait_remaining) * clock_resolution + ) - input_response_time self.wait_timeout[self.current_wait] = False self.logger.info( @@ -142,10 +151,12 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if fresh: self.smart_cache = {} + # fmt: off self.h5_file = h5file # store reference to h5 file for wait monitor self.current_wait = 0 # reset wait analysis self.started = False # Prevent status check from detecting previous wait values - # betwen now and when we actually send the start signal + # betwen now and when we actually send the start signal + # fmt: on # Get data from HDF5 file pulse_programs = [] diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index 0abcb601..c38fc70d 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -107,7 +107,8 @@ class PrawnBlaster(PseudoclockDevice): clock_resolution = 10e-9 # There appears to be ~50ns buffer on input and then we know there is 80ns between # trigger detection and first output pulse - trigger_delay = 130e-9 + input_response_time = 50e-9 + trigger_delay = input_response_time + 80e-9 # Overestimate that covers indefinite waits (which labscript does not yet support) trigger_minimum_duration = 160e-9 # There are 4 ASM instructions between end of pulse and being ready to detect @@ -129,6 +130,7 @@ class PrawnBlaster(PseudoclockDevice): "external_clock_pin", "clock_limit", "clock_resolution", + "input_response_time", "trigger_delay", "trigger_minimum_duration", "wait_delay", @@ -162,6 +164,7 @@ def __init__( factor = (1 / clock_frequency) / self.clock_resolution self.clock_limit *= factor self.clock_resolution *= factor + self.input_response_time *= factor self.trigger_delay *= factor self.trigger_minimum_duration *= factor self.wait_delay *= factor From 909d296c59c5836536c231c51c810631c392348e Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Fri, 9 Apr 2021 23:29:04 +1000 Subject: [PATCH 5/8] Fixed clock limit to handle updated PrawnBlaster specs --- labscript_devices/PrawnBlaster/labscript_devices.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index c38fc70d..61a556d6 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -103,7 +103,7 @@ def generate_code(self, *args, **kwargs): class PrawnBlaster(PseudoclockDevice): description = "PrawnBlaster" - clock_limit = 1 / 60e-9 + clock_limit = 1 / 50e-9 clock_resolution = 10e-9 # There appears to be ~50ns buffer on input and then we know there is 80ns between # trigger detection and first output pulse From 91b35c8ece077b023a05873ac7d8927ffafae37e Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Sat, 22 May 2021 13:43:57 +1000 Subject: [PATCH 6/8] Hopefully fixed the timing bug where PrawnBlaster pulses were twice as long as they should have been. Also fixed an incorrect max_instructions value --- .../PrawnBlaster/blacs_workers.py | 10 +++- .../PrawnBlaster/labscript_devices.py | 46 ++++++++++--------- .../PrawnBlaster/runviewer_parsers.py | 2 +- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index f7895a82..5576834c 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -70,7 +70,13 @@ def check_status(self): if response != "wait not yet available\r\n": # Parse the response from the PrawnBlaster wait_remaining = int(response) - clock_resolution = self.device_properties["clock_resolution"] + # Divide by two since the clock_resolution is for clock pulses, which + # have twice the clock_resolution of waits + # Technically, waits also only have a resolution of `clock_resolution` + # but the PrawnBlaster firmware accepts them in half of that so that + # they are easily converted to seconds via the clock frequency. + # Maybe this was a mistake, but it's done now. + clock_resolution = self.device_properties["clock_resolution"] / 2 input_response_time = self.device_properties["input_response_time"] timeout_length = round( self.wait_table[self.current_wait]["timeout"] / clock_resolution @@ -219,7 +225,7 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if self.smart_cache[pseudoclock][i] != instruction: self.prawnblaster.write( b"set %d %d %d %d\r\n" - % (pseudoclock, i, instruction["period"], instruction["reps"]) + % (pseudoclock, i, instruction["half_period"], instruction["reps"]) ) response = self.prawnblaster.readline().decode() assert ( diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index 61a556d6..6280fc47 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -103,19 +103,19 @@ def generate_code(self, *args, **kwargs): class PrawnBlaster(PseudoclockDevice): description = "PrawnBlaster" - clock_limit = 1 / 50e-9 - clock_resolution = 10e-9 + clock_limit = 1 / 100e-9 + clock_resolution = 20e-9 # There appears to be ~50ns buffer on input and then we know there is 80ns between # trigger detection and first output pulse input_response_time = 50e-9 trigger_delay = input_response_time + 80e-9 # Overestimate that covers indefinite waits (which labscript does not yet support) - trigger_minimum_duration = 160e-9 + trigger_minimum_duration = 160e-9 # There are 4 ASM instructions between end of pulse and being ready to detect # a retrigger wait_delay = 40e-9 allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock] - max_instructions = 60000 + max_instructions = 30000 @set_passed_properties( property_names={ @@ -160,8 +160,8 @@ def __init__( # Update the specs based on the number of pseudoclocks self.max_instructions = self.max_instructions // num_pseudoclocks # Update the specs based on the clock frequency - if self.clock_resolution != 1 / clock_frequency: - factor = (1 / clock_frequency) / self.clock_resolution + if self.clock_resolution != 2 / clock_frequency: + factor = (2 / clock_frequency) / self.clock_resolution self.clock_limit *= factor self.clock_resolution *= factor self.input_response_time *= factor @@ -258,7 +258,9 @@ def clocklines(self): def add_device(self, device): if len(self.child_devices) < ( self.num_pseudoclocks + self.use_wait_monitor - ) and isinstance(device, (_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock)): + ) and isinstance( + device, (_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock) + ): PseudoclockDevice.add_device(self, device) elif isinstance(device, _PrawnBlasterPseudoclock): raise LabscriptError( @@ -282,7 +284,7 @@ def generate_code(self, hdf5_file): for i, pseudoclock in enumerate(self.pseudoclocks): current_wait_index = 0 - # Compress clock instructions with the same period + # Compress clock instructions with the same half_period reduced_instructions = [] for instruction in pseudoclock.clock: if instruction == "WAIT": @@ -293,10 +295,12 @@ def generate_code(self, hdf5_file): wait_table[current_wait_index] ][1] current_wait_index += 1 - # The following period and reps indicates a wait instruction + # The following half_period and reps indicates a wait instruction reduced_instructions.append( { - "period": round(wait_timeout / self.clock_resolution), + "half_period": round( + wait_timeout / (self.clock_resolution / 2) + ), "reps": 0, } ) @@ -306,28 +310,28 @@ def generate_code(self, hdf5_file): # Two waits in a row are an indefinite wait reduced_instructions.append( { - "period": 2 ** 32 - 1, + "half_period": 2 ** 32 - 1, "reps": 0, } ) reduced_instructions.append( { - "period": 2 ** 32 - 1, + "half_period": 2 ** 32 - 1, "reps": 0, } ) # Normal instruction reps = instruction["reps"] - # period is in quantised units: - period = int(round(instruction["step"] / self.clock_resolution)) + # half_period is in quantised units: + half_period = int(round(instruction["step"] / self.clock_resolution)) if ( # If there is a previous instruction reduced_instructions # And it's not a wait and reduced_instructions[-1]["reps"] != 0 - # And the periods match - and reduced_instructions[-1]["period"] == period + # And the half_periods match + and reduced_instructions[-1]["half_period"] == half_period # And the sum of the previous reps and current reps won't push it over the limit and (reduced_instructions[-1]["reps"] + reps) < (2 ** 32 - 1) ): @@ -335,14 +339,14 @@ def generate_code(self, hdf5_file): reduced_instructions[-1]["reps"] += reps else: # New instruction - reduced_instructions.append({"period": period, "reps": reps}) + reduced_instructions.append({"half_period": half_period, "reps": reps}) # Only add this if there is room in the instruction table. The PrawnBlaster # firmware has extre room at the end for an instruction that is always 0 # and cannot be set over serial! if len(reduced_instructions) != self.max_instructions: - # The following period and reps indicates a stop instruction: - reduced_instructions.append({"period": 0, "reps": 0}) + # The following half_period and reps indicates a stop instruction: + reduced_instructions.append({"half_period": 0, "reps": 0}) # Check we have not exceeded the maximum number of supported instructions # for this number of speudoclocks @@ -352,10 +356,10 @@ def generate_code(self, hdf5_file): ) # Store these instructions to the h5 file: - dtypes = [("period", int), ("reps", int)] + dtypes = [("half_period", int), ("reps", int)] pulse_program = np.zeros(len(reduced_instructions), dtype=dtypes) for j, instruction in enumerate(reduced_instructions): - pulse_program[j]["period"] = instruction["period"] + pulse_program[j]["half_period"] = instruction["half_period"] pulse_program[j]["reps"] = instruction["reps"] group.create_dataset( f"PULSE_PROGRAM_{i}", compression=config.compression, data=pulse_program diff --git a/labscript_devices/PrawnBlaster/runviewer_parsers.py b/labscript_devices/PrawnBlaster/runviewer_parsers.py index f390b6c7..f1590005 100644 --- a/labscript_devices/PrawnBlaster/runviewer_parsers.py +++ b/labscript_devices/PrawnBlaster/runviewer_parsers.py @@ -82,7 +82,7 @@ def get_traces(self, add_trace, clock=None): for j in range(1, -1, -1): time.append(t) states.append(j) - t += row['period'] * clock_factor + t += row['half_period'] * clock_factor clock = (np.array(time), np.array(states)) From d7e530064011bb6528c6809a9a6f6268a429ba0c Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Thu, 27 May 2021 20:33:39 +1000 Subject: [PATCH 7/8] Fixed bug with abort and added code to handle the PrawnBlaster as a secondary pseudoclock --- .../PrawnBlaster/blacs_workers.py | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index 5576834c..75ae2b89 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -126,11 +126,15 @@ def check_status(self): else: waits_pending = True + run_status, clock_status = self.read_status() + return run_status, clock_status, waits_pending + + def read_status(self): self.prawnblaster.write(b"status\r\n") response = self.prawnblaster.readline().decode() match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response) if match: - return int(match.group(1)), int(match.group(2)), waits_pending + return int(match.group(1)), int(match.group(2)) elif response: raise Exception( f"PrawnBlaster is confused: saying '{response}' instead of 'run-status: clock-status:'" @@ -233,6 +237,10 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): ), f"PrawnBlaster said '{response}', expected 'ok'" self.smart_cache[pseudoclock][i] = instruction + if not self.is_master_pseudoclock: + # Start the Prawnblaster and have it wait for a hardware trigger + self.wait_for_trigger() + # All outputs end on 0 final = {} for pin in self.out_pins: @@ -249,6 +257,28 @@ def start_run(self): # set started = True self.started = True + def wait_for_trigger(self): + # Set to wait for trigger: + self.logger.info("sending hwstart") + self.prawnblaster.write(b"hwstart\r\n") + response = self.prawnblaster.readline().decode() + assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'" + + running = False + while not running: + run_status, clock_status = self.read_status() + # If we are running, great, the PrawnBlaster is waiting for a trigger + if run_status == 2: + running = True + # if we are not in TRANSITION_TO_RUNNING, then something has gone wrong + # and we should raise an exception + elif run_status != 1: + raise RuntimeError(f"Prawnblaster did not return an expected status. Status was {run_status}") + time.sleep(0.01) + + # set started = True + self.started = True + def transition_to_manual(self): if self.wait_table is not None: with h5py.File(self.h5_file, "a") as hdf5_file: @@ -278,11 +308,15 @@ def shutdown(self): self.prawnblaster.close() def abort_buffered(self): - self.prawnblaster.write(b"abort\r\n") - assert self.prawnblaster.readline().decode() == "ok\r\n" - # loop until abort complete - while self.check_status()[0] != 5: - time.sleep(0.5) + if not self.is_master_pseudoclock: + # Only need to send abort signal if we have told the PrawnBlaster to wait + # for a hardware trigger. Otherwise it's just been programmed with + # instructions and there is nothing we need to do to abort. + self.prawnblaster.write(b"abort\r\n") + assert self.prawnblaster.readline().decode() == "ok\r\n" + # loop until abort complete + while self.read_status()[0] != 5: + time.sleep(0.5) return True def abort_transition_to_buffered(self): From 1c7e633ce8565e69ac25764fb0d21e780934651d Mon Sep 17 00:00:00 2001 From: Phil Starkey Date: Fri, 28 May 2021 00:33:26 +1000 Subject: [PATCH 8/8] Fixed bug where transition_to_manual would complete when the PrawnBlaster was a secondary pseudoclock even if it hadn't finished (e.g. was still running or waiting for a trigger). Also formatted files with black. --- .../PrawnBlaster/blacs_workers.py | 27 +++++++++++++++++-- .../PrawnBlaster/labscript_devices.py | 4 ++- .../PrawnBlaster/runviewer_parsers.py | 20 +++++++------- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/labscript_devices/PrawnBlaster/blacs_workers.py b/labscript_devices/PrawnBlaster/blacs_workers.py index 75ae2b89..6c0bc957 100644 --- a/labscript_devices/PrawnBlaster/blacs_workers.py +++ b/labscript_devices/PrawnBlaster/blacs_workers.py @@ -229,7 +229,12 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh): if self.smart_cache[pseudoclock][i] != instruction: self.prawnblaster.write( b"set %d %d %d %d\r\n" - % (pseudoclock, i, instruction["half_period"], instruction["reps"]) + % ( + pseudoclock, + i, + instruction["half_period"], + instruction["reps"], + ) ) response = self.prawnblaster.readline().decode() assert ( @@ -273,7 +278,9 @@ def wait_for_trigger(self): # if we are not in TRANSITION_TO_RUNNING, then something has gone wrong # and we should raise an exception elif run_status != 1: - raise RuntimeError(f"Prawnblaster did not return an expected status. Status was {run_status}") + raise RuntimeError( + f"Prawnblaster did not return an expected status. Status was {run_status}" + ) time.sleep(0.01) # set started = True @@ -302,6 +309,22 @@ def transition_to_manual(self): hdf5_file.create_dataset("/data/waits", data=data) self.wait_durations_analysed.post(self.h5_file) + + # If PrawnBlaster is master pseudoclock, then it will have it's status checked + # in the BLACS tab status check before any transition to manual is called. + # However, if it's not the master pseudoclock, we need to check here instead! + if not self.is_master_pseudoclock: + # Wait until shot completes + while True: + run_status, clock_status = self.read_status() + if run_status == 0: + break + if run_status in [3, 4, 5]: + raise RuntimeError( + f"Prawnblaster status returned run-status={run_status} during transition to manual" + ) + time.sleep(0.01) + return True def shutdown(self): diff --git a/labscript_devices/PrawnBlaster/labscript_devices.py b/labscript_devices/PrawnBlaster/labscript_devices.py index 6280fc47..eb05b79c 100644 --- a/labscript_devices/PrawnBlaster/labscript_devices.py +++ b/labscript_devices/PrawnBlaster/labscript_devices.py @@ -339,7 +339,9 @@ def generate_code(self, hdf5_file): reduced_instructions[-1]["reps"] += reps else: # New instruction - reduced_instructions.append({"half_period": half_period, "reps": reps}) + reduced_instructions.append( + {"half_period": half_period, "reps": reps} + ) # Only add this if there is room in the instruction table. The PrawnBlaster # firmware has extre room at the end for an instruction that is always 0 diff --git a/labscript_devices/PrawnBlaster/runviewer_parsers.py b/labscript_devices/PrawnBlaster/runviewer_parsers.py index f1590005..42f9e552 100644 --- a/labscript_devices/PrawnBlaster/runviewer_parsers.py +++ b/labscript_devices/PrawnBlaster/runviewer_parsers.py @@ -37,10 +37,10 @@ def get_traces(self, add_trace, clock=None): # get the pulse program pulse_programs = [] - with h5py.File(self.path, 'r') as f: + with h5py.File(self.path, "r") as f: # Get the device properties - device_props = properties.get(f, self.name, 'device_properties') - conn_props = properties.get(f, self.name, 'connection_table_properties') + device_props = properties.get(f, self.name, "device_properties") + conn_props = properties.get(f, self.name, "connection_table_properties") self.clock_resolution = device_props["clock_resolution"] self.trigger_delay = device_props["trigger_delay"] @@ -49,8 +49,8 @@ def get_traces(self, add_trace, clock=None): # Extract the pulse programs num_pseudoclocks = conn_props["num_pseudoclocks"] for i in range(num_pseudoclocks): - pulse_programs.append(f[f'devices/{self.name}/PULSE_PROGRAM_{i}'][:]) - + pulse_programs.append(f[f"devices/{self.name}/PULSE_PROGRAM_{i}"][:]) + # Generate clocklines and triggers clocklines_and_triggers = {} for pulse_program in pulse_programs: @@ -64,7 +64,7 @@ def get_traces(self, add_trace, clock=None): last_instruction_was_wait = False for row in pulse_program: - if row['reps'] == 0 and not last_instruction_was_wait: # WAIT + if row["reps"] == 0 and not last_instruction_was_wait: # WAIT last_instruction_was_wait = True if clock is not None: t = clock_ticks[trigger_index] + self.trigger_delay @@ -78,11 +78,11 @@ def get_traces(self, add_trace, clock=None): continue else: last_instruction_was_wait = False - for i in range(row['reps']): + for i in range(row["reps"]): for j in range(1, -1, -1): time.append(t) states.append(j) - t += row['half_period'] * clock_factor + t += row["half_period"] * clock_factor clock = (np.array(time), np.array(states)) @@ -91,6 +91,8 @@ def get_traces(self, add_trace, clock=None): # Ignore the dummy internal wait monitor clockline if clock_line.parent_port.startswith("GPIO"): clocklines_and_triggers[clock_line_name] = clock - add_trace(clock_line_name, clock, self.name, clock_line.parent_port) + add_trace( + clock_line_name, clock, self.name, clock_line.parent_port + ) return clocklines_and_triggers