From ce147e936221b68332a9013d7b5ac2f2db7a263d Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 23 May 2024 13:53:27 -0400 Subject: [PATCH 01/10] Add PrawnDO labscript device code for use with the PrawnDO digital output board. --- labscript_devices/PrawnDO/__init__.py | 12 + labscript_devices/PrawnDO/blacs_tabs.py | 41 ++ labscript_devices/PrawnDO/blacs_workers.py | 354 ++++++++++++++++++ .../PrawnDO/labscript_devices.py | 292 +++++++++++++++ labscript_devices/PrawnDO/register_classes.py | 26 ++ .../PrawnDO/runviewer_parsers.py | 102 +++++ 6 files changed, 827 insertions(+) create mode 100644 labscript_devices/PrawnDO/__init__.py create mode 100644 labscript_devices/PrawnDO/blacs_tabs.py create mode 100644 labscript_devices/PrawnDO/blacs_workers.py create mode 100644 labscript_devices/PrawnDO/labscript_devices.py create mode 100644 labscript_devices/PrawnDO/register_classes.py create mode 100644 labscript_devices/PrawnDO/runviewer_parsers.py diff --git a/labscript_devices/PrawnDO/__init__.py b/labscript_devices/PrawnDO/__init__.py new file mode 100644 index 0000000..9a84bcb --- /dev/null +++ b/labscript_devices/PrawnDO/__init__.py @@ -0,0 +1,12 @@ +##################################################################### +# # +# /labscript_devices/PrawnDO/__init__.py # +# # +# Copyright 2023, Philip Starkey, Carter Turnbaugh, Patrick Miller # +# # +# 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. # +# # +##################################################################### \ No newline at end of file diff --git a/labscript_devices/PrawnDO/blacs_tabs.py b/labscript_devices/PrawnDO/blacs_tabs.py new file mode 100644 index 0000000..22a301f --- /dev/null +++ b/labscript_devices/PrawnDO/blacs_tabs.py @@ -0,0 +1,41 @@ +##################################################################### +# # +# /labscript_devices/PrawnDO/blacs_tabs.py # +# # +# Copyright 2023, Philip Starkey, Carter Turnbaugh, Patrick Miller # +# # +# 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 + +class PrawnDOTab(DeviceTab): + def initialise_GUI(self): + do_prop = {} + for i in range(0, 16): + do_prop['0x{:01X}'.format(i)] = {} + self.create_digital_outputs(do_prop) + + _, _, do_widgets = self.auto_create_widgets() + self.auto_place_widgets(do_widgets) + + device = self.settings['connection_table'].find_by_name(self.device_name) + + self.com_port = device.properties['com_port'] + + self.supports_remote_value_check(True) + self.supports_smart_programming(True) + + + def initialise_workers(self): + self.create_worker( + "main_worker", + "naqslab_devices.prawn_digital_output_labscript.blacs_workers.PrawnDOWorker", + { + 'com_port': self.com_port, + }, + ) + self.primary_worker = "main_worker" diff --git a/labscript_devices/PrawnDO/blacs_workers.py b/labscript_devices/PrawnDO/blacs_workers.py new file mode 100644 index 0000000..e28f54d --- /dev/null +++ b/labscript_devices/PrawnDO/blacs_workers.py @@ -0,0 +1,354 @@ +##################################################################### +# # +# /labscript_devices/PrawnDO/blacs_workers.py # +# # +# Copyright 2023, Philip Starkey, Carter Turnbaugh, Patrick Miller # +# # +# 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.tab_base_classes import Worker +import labscript_utils.h5_lock, h5py +import labscript_utils +from labscript import LabscriptError +import numpy as np +import re +import time + +class PrawnDOInterface(object): + + min_version = (1, 2, 0) + """Minimum compatible firmware version tuple""" + + def __init__(self, com_port): + global serial; import serial + global struct; import struct + + self.timeout = 0.2 + self.conn = serial.Serial(com_port, 10000000, timeout=self.timeout) + + version = self.get_version() + print(f'Connected to version: {version}') + # ensure firmware is compatible + assert version >= self.min_version, f'Incompatible firmware, must be >= {self.min_version}' + + current_status = self.status() + print(f'Current status is {current_status}') + + def get_version(self): + + self.conn.write(b'ver\r\n') + version_str = self.conn.readline().decode() + assert version_str.startswith("Version: ") + version = tuple(int(i) for i in version_str[9:].split('.')) + assert len(version) == 3 + + return version + + def _read_full_buffer(self): + '''Used to get any extra lines from device after a failed send_command''' + + resp = self.conn.readlines() + str_resp = ''.join([st.decode() for st in resp]) + + return str_resp + + def send_command(self, command, readlines=False): + '''Sends the supplied string command and checks for a response. + + Automatically applies the correct termination characters. + + Args: + command (str): Command to send. Termination and encoding is done automatically. + readlines (bool, optional): Use pyserial's readlines functionality to read multiple + response lines. Slower as it relies on timeout to terminate reading. + + Returns: + str: String response from the PrawnDO + ''' + command += '\r\n' + self.conn.write(command.encode()) + + if readlines: + str_resp = self._read_full_buffer() + else: + str_resp = self.conn.readline().decode() + + return str_resp + + def send_command_ok(self, command): + '''Sends the supplied string command and confirms 'ok' response. + + Args: + command (str): String command to send. + + Raises: + LabscriptError: If response is not `ok\\r\\n` + ''' + + resp = self.send_command(command) + if resp != 'ok\r\n': + # get complete error message + resp += self._read_full_buffer() + raise LabscriptError(f"Command '{command:s}' failed. Got response '{repr(resp)}'") + + def status(self): + '''Reads the status of the PrawnDO + + Returns: + (int, int): tuple containing + + - **run-status** (int): Run status code + - **clock-status** (int): Clock status code + + Raises: + LabscriptError: If response is not `ok\\r\\n` + ''' + resp = self.send_command('sts') + match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", resp) + if match: + return int(match.group(1)), int(match.group(2)) + else: + resp += self._read_full_buffer() + raise LabscriptError(f'PrawnDO invalid status, returned {repr(resp)}') + + def output_state(self): + '''Reads the current output state of the PrawnDO + + Returns: + int: Output state of all 16 bits + + Raises: + LabscriptError: If response is not `ok\\r\\n` + ''' + + resp = self.send_command('gto') + + try: + resp_i = int(resp, 16) + except Exception as e: + resp += self._read_full_buffer() + raise LabscriptError(f'Remote value check failed. Got response {repr(resp)}') from e + + return resp_i + + def adm_batch(self, pulse_program): + '''Sends pulse program as single binary block using `adm` command. + + Args: + pulse_program (numpy.ndarray): Structured array of program to send. + Must have first column as bit sets (> i) & 1) for i in range(16)} + + def check_status(self): + '''Checks operational status of the PrawnDO. + + Automatically called by BLACS to update status. + + Returns: + (int, int): Tuple containing: + + - **run-status** (int): Possible values are: + + * 0 : manual mode + * 1 : transitioning to buffered execution + * 2 : buffered execution + * 3 : abort requested + * 4 : aborting buffered execution + * 5 : last buffered execution aborted + * 6 : transitioning to manual mode + + - **clock-status** (int): Possible values are: + + * 0 : internal clock + * 1 : external clock + ''' + + return self.intf.status() + + def program_manual(self, front_panel_values): + """Change output states in manual mode. + + Returns: + dict: Output states after command execution. + """ + value = self._dict_to_int(front_panel_values) + # send static state + self.intf.send_command_ok(f'man {value:04x}') + # confirm state set correctly + resp_i = self.intf.output_state() + + return self._int_to_dict(resp_i) + + def check_remote_values(self): + """Checks the remote state of the PrawnDO. + + Called automatically by BLACS. + + Returns: + dict: Dictionary of the digital output states. + """ + + resp_i = self.intf.output_state() + + return self._int_to_dict(resp_i) + + def transition_to_buffered(self, device_name, h5file, initial_values, fresh): + + if fresh: + self.smart_cache = {'pulse_program':None} + + with h5py.File(h5file, 'r') as hdf5_file: + group = hdf5_file['devices'][device_name] + if 'pulse_program' not in group: + # if no output commanded, return + return + self.device_properties = labscript_utils.properties.get( + hdf5_file, device_name, "device_properties") + pulse_program = group['pulse_program'][()] + + # configure clock from device properties + ext = self.device_properties['external_clock'] + freq = self.device_properties['clock_frequency'] + self.intf.send_command_ok(f"clk {ext:d} {freq:.0f}") + + # check if it is more efficient to fully refresh + if not fresh and self.smart_cache['pulse_program'] is not None: + + # get more convenient handle to smart cache array + curr_program = self.smart_cache['pulse_program'] + + # if arrays aren't of same shape, only compare up to smaller array size + n_curr = len(curr_program) + n_new = len(pulse_program) + if n_curr > n_new: + # technically don't need to reprogram current elements beyond end of new elements + new_inst = np.sum(curr_program[:n_new] != pulse_program) + elif n_curr < n_new: + n_diff = n_new - n_curr + val_diffs = np.sum(curr_program != pulse_program[:n_curr]) + new_inst = val_diffs + n_diff + else: + new_inst = np.sum(curr_program != pulse_program) + + if new_inst / n_new > 0.1: + fresh = True + + # if fresh or not smart cache, program full table as a batch + # this is faster than going line by line + if fresh or self.smart_cache['pulse_program'] is None: + self.intf.send_command_ok('cls') # clear old program + self.intf.adm_batch(pulse_program) + self.smart_cache['pulse_program'] = pulse_program + else: + # only program table lines that have changed + n_cache = len(self.smart_cache['pulse_program']) + for i, instr in enumerate(pulse_program): + if i >= n_cache: + print(f'programming step {i}') + self.intf.send_command_ok(f'set {i:x} {instr[0]:x} {instr[1]:x}') + self.smart_cache['pulse_program'][i] = instr + + elif (self.smart_cache['pulse_program'][i] != instr): + + print(f'programming step {i}') + self.intf.send_command_ok(f'set {i:x} {instr[0]:x} {instr[1]:x}') + self.smart_cache['pulse_program'][i] = instr + + final_values = self._int_to_dict(pulse_program[-1][0]) + + # start program, waiting for beginning trigger from parent + self.intf.send_command_ok('run') + + return final_values + + def transition_to_manual(self): + """Transition to manual mode after buffered execution completion. + + Returns: + bool: `True` if transition to manual is successful. + """ + i = 0 + while True: + run_status, _ = self.intf.status() + i += 1 + if run_status == 0: + return True + elif i == 1000: + # program hasn't ended, probably bad triggering + # abort and raise an error + self.abort_buffered() + raise LabscriptError(f'PrawnDO did not end with status {run_status:d}. Is triggering working?') + elif run_status in [3,4,5]: + raise LabscriptError(f'PrawnDO returned status {run_status} in transition to manual') + + def abort_buffered(self): + """Aborts a currently running buffered execution. + + Returns: + bool: `True` is abort was successful. + """ + self.intf.send_command_ok('abt') + # loop until abort complete + while self.intf.status()[0] != 5: + time.sleep(0.5) + return True + + def abort_transition_to_buffered(self): + """Aborts transition to buffered. + + Calls :meth:`abort_buffered` + """ + return self.abort_buffered() + + def shutdown(self): + """Closes serial connection to PrawnDO""" + self.intf.close() diff --git a/labscript_devices/PrawnDO/labscript_devices.py b/labscript_devices/PrawnDO/labscript_devices.py new file mode 100644 index 0000000..514da64 --- /dev/null +++ b/labscript_devices/PrawnDO/labscript_devices.py @@ -0,0 +1,292 @@ +##################################################################### +# # +# /labscript_devices/PrawnDO/labscript_devices.py # +# # +# Copyright 2023, Philip Starkey, Carter Turnbaugh, Patrick Miller # +# # +# 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 import ( + IntermediateDevice, + PseudoclockDevice, + Pseudoclock, + ClockLine, + DigitalOut, + Trigger, + bitfield, + set_passed_properties, + LabscriptError +) +import numpy as np + +class _PrawnDOPseudoclock(Pseudoclock): + """Dummy pseudoclock for use with PrawnDO. + + This pseudoclock ensures only one clockline is attached. + """ + + def add_device(self, device): + + if not isinstance(device, _PrawnDOClockline) or self.child_devices: + # only allow one child dummy clockline + raise LabscriptError("You are trying to access the special, dummy, Pseudoclock of the PrawnDO " + f"{self.parent_device.name}. This is for internal use only.") + else: + Pseudoclock.add_device(self, device) + + +class _PrawnDOClockline(ClockLine): + """Dummy clockline for use with PrawnDO + + Ensures only a single _PrawnDODirectOutputs is connected to the PrawnDO + """ + + def add_device(self, device): + + if not isinstance(device, _PrawnDigitalOutputs) or self.child_devices: + # only allow one child device + raise LabscriptError("You are trying to access the special, dummy, Clockline of the PrawnDO " + f"{self.pseudoclock_device.name}. This is for internal use only.") + else: + ClockLine.add_device(self, device) + + +class _PrawnDigitalOutputs(IntermediateDevice): + allowed_children = [DigitalOut] + + allowed_channels = ('0', '1', '2', '3', + '4', '5', '6', '7', + '8', '9', 'A', 'B', + 'C', 'D', 'E', 'F') + + def __init__(self, name, parent_device, + **kwargs): + """Collective output class for the PrawnDO. + + This class aggregates the 16 individual digital outputs of the PrawnDO. + It is for internal use of the PrawnDO only. + + Args: + name (str): name to assign + parent_device (Device): Parent device PrawnDO is connected to + """ + + IntermediateDevice.__init__(self, name, parent_device, **kwargs) + self.connected_channels = [] + + def add_device(self, device): + """Confirms channel specified is valid before adding + + Args: + device (): Device to attach. Must be a digital output. + Allowed connections are a string that ends with a 0-F hex + channel number. + """ + + conn = device.connection + chan = conn.split(' ')[-1] + + if chan not in self.allowed_channels: + raise LabscriptError(f'Invalid channel specification: {conn}') + if chan in self.connected_channels: + raise LabscriptError(f'Channel {conn} already connected to {self.parent_device.name}') + + self.connected_channels.append(chan) + super().add_device(device) + + +class PrawnDO(PseudoclockDevice): + description = "PrawnDO device" + + # default specs assuming 100MHz system clock + clock_limit = 1 / 100e-9 + "Maximum allowable clock rate" + clock_resolution = 10e-9 + "Minimum resolvable unit of time, corresponsd to system clock period." + minimum_duration = 50e-9 + "Minimum time between updates on the outputs." + wait_delay = 50e-9 + "Minimum required length of wait before a retrigger can be detected." + input_response_time = 50e-9 + "Time between hardware trigger and output starting." + trigger_delay = 50e-9 # TODO: gets applied twice on waits... + trigger_minimum_duration = 160e-9 + "Minimum required duration of hardware trigger. A fairly large over-estimate." + + allowed_children = [_PrawnDOPseudoclock] + + max_instructions = 30000 + """Maximum number of instructions. Set by zmq timeout when sending the commands.""" + + @set_passed_properties( + property_names={ + 'connection_table_properties': [ + 'com_port', + ], + 'device_properties': [ + 'clock_frequency', + 'external_clock', + 'clock_limit', + 'clock_resolution', + 'minimum_duration', + 'input_response_time', + 'trigger_delay', + 'trigger_minimum_duration', + 'wait_delay', + ] + } + ) + def __init__(self, name, + trigger_device = None, + trigger_connection = None, + clock_line = None, + com_port = 'COM1', + clock_frequency = 100e6, + external_clock = False, + ): + """PrawnDO digital output device. + + This labscript device provides general purpose digital outputs + using a Raspberry Pi Pico with custom firmware. + + It supports two types of connections to a parent device: + direct to a :class:`~.Clockline` via the `clock_line` argument or + through a :class:`~.Trigger` from an :class:`~.IntermediateDevice` + via the `trigger_device` and `trigger_connection` arguments. + Only one should be supplied. + + + Args: + name (str): python variable name to assign to the PrawnDO + trigger_device (:class:`~.IntermediateDevice`, optional): + Device that will send the starting hardware trigger. + Used when connecting to an `IntermediateDevice` via a `DigitalOut`. + trigger_connection (str, optional): Which output of the `trigger_device` + is connected to the PrawnDO hardware trigger input. + Not required when directly connected to a `Clockline`. + clock_line (:class:`~.Clockline`, optional): + Used when connected directly to a `Clockline`. + Not required if using a trigger device. + com_port (str): COM port assinged to the PrawnDO by the OS. + Takes the form of `COMd` where `d` is an integer. + clock_frequency (float, optional): System clock frequency, in Hz. + Must be less than 133 MHz. Default is `100e6`. + external_clock (bool, optional): Whether to use an external clock. + Default is `False`. + """ + + if clock_frequency > 133e6: + raise ValueError('Clock frequency must be less than 133 MHz') + + self.external_clock = external_clock + self.clock_frequency = clock_frequency + # update specs based on clock frequency + if self.clock_frequency != 100e6: + # factor to scale times by + factor = 100e6/self.clock_frequency + self.clock_limit *= factor + self.clock_resolution *= factor + self.minimum_duration *= factor + self.wait_delay *= factor + self.input_response_time *= factor + self.trigger_delay *= factor + self.trigger_minimum_duration *= factor + + if clock_line is not None and trigger_device is not None: + raise LabscriptError("Provide only a trigger_device or a clock_line, not both") + if clock_line is not None: + # make internal Intermediate device and trigger to connect it + self.__intermediate = _PrawnDOIntermediateDevice(f'{name:s}__intermediate', + clock_line) + PseudoclockDevice.__init__(self, name, self.__intermediate, 'internal') + else: + # normal pseudoclock device triggering + PseudoclockDevice.__init__(self, name, trigger_device, trigger_connection) + + # set up internal connections to allow digital outputs + self.__pseudoclock = _PrawnDOPseudoclock(f'{name:s}__pseudoclock', self, '_') + self.__clockline = _PrawnDOClockline(f'{name:s}__clockline', + self.__pseudoclock, '_') + self.outputs = _PrawnDigitalOutputs(f'{name:s}__pod', self.__clockline) + + self.BLACS_connection = com_port + + def add_device(self, device): + + if isinstance(device, _PrawnDOPseudoclock): + super().add_device(device) + elif isinstance(device, DigitalOut): + raise LabscriptError(f"Digital outputs must be connected to {self.name:s}.outputs") + else: + raise LabscriptError(f"You have connected unsupported {device.name:s} (class {device.__class__}) " + f"to {self.name:s}") + + + def generate_code(self, hdf5_file): + PseudoclockDevice.generate_code(self, hdf5_file) + + bits = [0] * 16 # Start with a list of 16 zeros + # Isolating the Pod child device in order to access the output change + # times to store in the array + + # Retrieving all of the outputs contained within the pods and + # collecting/consolidating the times when they change + outputs = self.get_all_outputs() + times = self.__pseudoclock.times[self.__clockline] + instructions = self.__pseudoclock.clock + if len(times) == 0: + # no instructions, so return + return + + # get where wait instructions should be added from clock instructions + wait_idxs = [i for i,instr in enumerate(instructions) if instr=='WAIT'] + + # Retrieving the time series of each DigitalOut to be stored + # as the output word for the pins + for output in outputs: + output.make_timeseries(times) + chan = output.connection.split(' ')[-1] + bits[int(chan, 16)] = np.asarray(output.timeseries, dtype = np.uint16) + # Merge list of lists into an array with a single 16 bit integer column + bit_sets = np.array(bitfield(bits, dtype=np.uint16)) + + # Now create the reps array (ie times between changes in number of clock cycles) + reps = np.rint(np.diff(times)/self.clock_resolution).astype(np.uint32) + + # add stop command sequence + # final output already in bit_sets + reps = np.append(reps, 0) # causes last instruction to hold + # next two indicate the stop + bit_sets = np.append(bit_sets, 0) # this value is ignored + reps = np.append(reps, 0) + + # Add in wait instructions to reps + # have output maintain previous output state during wait + reps = np.insert(reps, wait_idxs, 0) + bit_sets = np.insert(bit_sets, wait_idxs, bit_sets[wait_idxs]) + + # Raising an error if the user adds too many commands + if reps.size > self.max_instructions: + raise LabscriptError ( + "Too Many Commands" + ) + + group = hdf5_file['devices'].require_group(self.name) + # combining reps and bit sets into single structured array for saving to hdf5 file + dtype = np.dtype([('bit_sets', ' Date: Thu, 23 May 2024 13:56:59 -0400 Subject: [PATCH 02/10] Add PrawnDO docs --- docs/source/devices.rst | 1 + docs/source/devices/prawndo.rst | 211 ++++++++++++++++++++++++++++++++ 2 files changed, 212 insertions(+) create mode 100644 docs/source/devices/prawndo.rst diff --git a/docs/source/devices.rst b/docs/source/devices.rst index 6237a18..1b42ec0 100644 --- a/docs/source/devices.rst +++ b/docs/source/devices.rst @@ -19,6 +19,7 @@ Many pseudoclock devices also include other types of outputs, including digital devices/opalkellyXEM3001 devices/pineblaster devices/prawnblaster + devices/prawndo devices/rfblaster NI DAQS diff --git a/docs/source/devices/prawndo.rst b/docs/source/devices/prawndo.rst new file mode 100644 index 0000000..e162e74 --- /dev/null +++ b/docs/source/devices/prawndo.rst @@ -0,0 +1,211 @@ +PrawnDO +======= + +This labscript device controls the `PrawnDO `_ +open-source digital output generator based on the +`Raspberry Pi Pico `_ platform. +It is designed to be a companion device to the :doc:`PrawnBlaster ` allowing for +arbitrary digital output specification (in contrast to the variable pseudoclock generation of the PrawnBlaster). + +Initial code development was in this `repository `_. + +Specifications +~~~~~~~~~~~~~~ + +The PrawnDO takes advantage of the specs of the Pico to provide the following: + +* 16 synchronous digital outputs with timing specs equivalent to the PrawnBlaster + + - Timing resolution for an update is 1 clock cycle (10 ns at default 100 MHz clock) + - Minimum time between updates (on any output) is 5 clock cycles (50 ns with 100 MHz clock) + - Maximum time between updates (on any output) is 2^32-1 clock cycles (~42.9 s with 100 MHz clock) + - Updates are internally timed (ie only initial triggering is needed, not for every update) + +* 30,000 instructions (where each instruction can be held between 5 and 2^32-1 clock cycles) +* Support for external hardware triggers to begin and re-start execution after a wait. +* Can be referenced to an external LVCMOS clock +* Internal clock can be set up to 133 MHz (which scales timing specs accordingly) + + +Installation +~~~~~~~~~~~~ + +In order to turn the standard Pico into a PrawnDO, you need to load the custom firmware +available in the `Github repo `_ onto the board. +The simplest way to do this is by holding the reset button on the board while plugging the USB into a computer. +This will bring up a mounted folder that you copy-paste the firmware to. +Once copied, the board will reset and be ready to go. + +Note that this device communicates using a virtual COM port. +The number is assigned by the controlling computer and will need to be determined in order for BLACS to connect to the PrawnDO. + + +Usage +~~~~~ + +The pinout for the PrawnDO is as follows: + +* Outputs 0-15 (labelled by default in hex 0-F): GPIO pins 0-15, respectively. +* External Trigger input: GPIO 16 +* External Clock input: GPIO 20 + +Note that signal cables should be connected to the Pico digital grounds for proper operation. + +The PrawnDO can provide up to 16 digital outputs, which are accessed via `name.outputs`. +Each channel is specified using the corresponding hex character (spanning 0-F for 0-15). +The channel string must end with a single character between 0-F to be valid +(i.e. `'flag 0'`, `'do 0'`, and `'0'` are all valid channel specifications for GPIO 0 of the PrawnDO). + +An example connection table that uses the PrawnBlaster and PrawnDO: + +.. code-block:: python + + from labscript import * + + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.PrawnDO.labscript_devices import PrawnDO + + PrawnBlaster(name='prawn', com_port='COM6', num_pseudoclocks=1) + + PrawnDO(name='prawn_do', com_port='COM5', clock_line=prawn.clocklines[0]) + + DigitalOut('do0', prawn_do.outputs, 'flag 0') + DigitalOut('do1', prawn_do.outputs, 'chan 1') + DigitalOut('do10', prawn_do.outputs, 'flag C') + + if __name__ == "__main__": + + start() + + stop(1) + +.. note:: + + The PrawnDO is designed to be directly connected to a Clockline, + something not normally done for internally-timed devices in labscript. + This is merely for simplicity under the most typical use case of + adding standard digital output capability to a PrawnBlaster master pseudoclocking device. + + When used in this way, the PrawnDO can share the Clockline with other devices, + especially with other PrawnDO boards allowing for significant fan-out. + Nominally, the PrawnDO will ignore clock ticks from other devices on the same Clockline, + such as a DAQ. + However, standard cautions should be taken when sharing a clockline between devices + (i.e. don't overload the physical output driver with too many parallel devices, + limit the number of devices doing fast things at nearly the same times, + validate critical timings/operations independently). + +The PrawnDO can also be triggerd from a standard DigitalOut Trigger. +In this case, the `clock_line` argument is not used, +but the standard `trigger_device` and `trigger_connection` arguments. + +Synchronization +--------------- + +The PrawnDO generates output based on internal timing with external starting triggers +in a manner nearly equivalent to the PrawnBlaster. +This means that under a typical use case of a PrawnBlaster used with a PrawnDO, +the output timings of the devices will drift as their internal clocks drift. +Each Pico is specified to have a clock with better than 50 ppm stability, +meaning drift could be as bad as 100 ppm between two devices +(e.g. 100 microsecond drift after 1 second of run time). +In practice, relative drift is often around 5 ppm. + +To overcome this, either use labscript waits right before time-sensitive operations +to resynchronize back to within a single clock cycle (:math:`\pm10` ns), +or use a common external clock for both devices. + +Unless buffering/level protecting circuitry is used, +both the PrawnBlaster and the PrawnDO require LVCMOS square-wave clock signals. +An example evaluation board with a flexible, multi-channel LVCMOS clock generator is +the SI535X-B20QFN-EVB. +Note that interrupting the external clock can cause the Pico serial communication to freeze. +Recovery requires resetting the board via a power cycle or shorting the RUN pin to ground +to re-enable default options including the internal clock. + +An example connection table using external clocks with the default frequency of 100 MHz is: + +.. code-block:: python + + from labscript import * + + from labscript_devices.PrawnBlaster.labscript_devices import PrawnBlaster + from labscript_devices.PrawnDO.labscript_devices import PrawnDO + + PrawnBlaster(name='prawn', com_port='COM6', num_pseudoclocks=1, + external_clock_pin=20) + + PrawnDO(name='prawn_do', com_port='COM5', clock_line=prawn.clocklines[0], + external_clock=True) + + DigitalOut('do0', prawn_do.outputs, 'flag 0') + DigitalOut('do1', prawn_do.outputs, 'chan 1') + DigitalOut('do10', prawn_do.outputs, 'flag C') + + if __name__ == "__main__": + + start() + + stop(1) + + +Input/Output Buffers +-------------------- + +While the PrawnBlaster and PrawnDO boards can be used as is, +it is often a good idea to add unity-gain channel buffers to the inputs and outputs. +Using buffers and line drivers from a LVCMOS family with 5V/TTL tolerant inputs can provide +compatibility with TTL inputs and drive higher capacitance loads (such a long BNC cables) more reliably. +An example that implements these buffers can be found `here `_. + +Waits +----- + +All waits in the PrawnDO are indefinite waits in the parlance of the PrawnBlaster. +This means they will never time out, but must have an external trigger to restart execution. +Changing a digital output state concurrently with a wait +results in the PrawnDO output holding the updated value during the wait. +For example, in the following code snippet, the output of `do0` will be low during the wait. +For the output of `do0` to remain high during the wait, +the second instruction (`do0.go_low(t)`) must be at least 5 clock cycles after the wait start time. + +.. code-block:: python + + t = 0 + do.go_high(t) + t = 1e-3 + wait('my_wait', t) + do0.go_low(t) + +Detailed Documentation +~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: labscript_devices.PrawnDO + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnDO.labscript_devices + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnDO.blacs_tabs + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnDO.blacs_workers + :members: + :undoc-members: + :show-inheritance: + :private-members: + +.. automodule:: labscript_devices.PrawnDO.runviewer_parsers + :members: + :undoc-members: + :show-inheritance: + :private-members: \ No newline at end of file From b830f0e6bd52421dbcc977b805437207f4eef652 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 23 May 2024 13:57:21 -0400 Subject: [PATCH 03/10] Minor updates to the Prawnblaster docs. --- docs/source/devices/prawnblaster.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index c491bf8..8d19473 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -2,6 +2,8 @@ PrawnBlaster ============ This labscript device controls the `PrawnBlaster `_ open-source digital pattern generator based on the `Raspberry Pi Pico `_ platform. +It is designed to produce pseudoclock timing pulses. +See the companion :doc:`PrawnDO ` device for arbitary pulse generation. Specifications ~~~~~~~~~~~~~~ @@ -30,7 +32,7 @@ The PrawnBlaster takes advantage of the specs of the Pico to provide the followi Installation ~~~~~~~~~~~~ -In order to turn the standard Pico into a PrawnBlaster, you need to load the custom firmware available in the `Github repo `_ onto the board. +In order to turn the standard Pico into a PrawnBlaster, you need to load the custom firmware available in the `Github repo `_ onto the board. The simplest way to do this is by holding the reset button on the board while plugging the USB into a computer. This will bring up a mounted folder that you copy-paste the firmware to. Once copied, the board will reset and be ready to go. @@ -79,6 +81,16 @@ An example connection table that uses the PrawnBlaster: stop(1) +Input/Output Buffers +-------------------- + +While the PrawnBlaster and PrawnDO boards can be used as is, +it is often a good idea to add unity-gain channel buffers to the inputs and outputs. +Using buffers and line drivers from a LVCMOS family with 5V/TTL tolerant inputs can provide +compatibility with TTL inputs and drive higher capacitance loads (such a long BNC cables) more reliably. +Examples that implement these buffers can be found `here `_ +and `here `_. + Detailed Documentation ~~~~~~~~~~~~~~~~~~~~~~ From 7c74fd62fddb137d7c04fcec4f15f610ed7d285a Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 8 Aug 2024 16:50:11 -0400 Subject: [PATCH 04/10] Fix imports --- labscript_devices/PrawnDO/blacs_tabs.py | 2 +- labscript_devices/PrawnDO/register_classes.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/labscript_devices/PrawnDO/blacs_tabs.py b/labscript_devices/PrawnDO/blacs_tabs.py index 22a301f..15533b4 100644 --- a/labscript_devices/PrawnDO/blacs_tabs.py +++ b/labscript_devices/PrawnDO/blacs_tabs.py @@ -33,7 +33,7 @@ def initialise_GUI(self): def initialise_workers(self): self.create_worker( "main_worker", - "naqslab_devices.prawn_digital_output_labscript.blacs_workers.PrawnDOWorker", + "labscript_devices.PrawnDO.blacs_workers.PrawnDOWorker", { 'com_port': self.com_port, }, diff --git a/labscript_devices/PrawnDO/register_classes.py b/labscript_devices/PrawnDO/register_classes.py index 111a6e3..a935660 100644 --- a/labscript_devices/PrawnDO/register_classes.py +++ b/labscript_devices/PrawnDO/register_classes.py @@ -15,7 +15,7 @@ register_classes( 'PrawnDO', BLACS_tab='labscript_devices.PrawnDO.blacs_tabs.PrawnDOTab', - runviewer_parser='naqslab_devices.PrawnDO.runviewer_parsers.PrawnDOParser', + runviewer_parser='labscript_devices.PrawnDO.runviewer_parsers.PrawnDOParser', ) # private shim class necessary from runviewer parsing of shots From 1b7ca5a3da11386b8219bf498255241dad199157 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Thu, 8 Aug 2024 17:05:58 -0400 Subject: [PATCH 05/10] Properly allow the BLACSTab to find child digital outputs. --- labscript_devices/PrawnDO/blacs_tabs.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/labscript_devices/PrawnDO/blacs_tabs.py b/labscript_devices/PrawnDO/blacs_tabs.py index 15533b4..547cdac 100644 --- a/labscript_devices/PrawnDO/blacs_tabs.py +++ b/labscript_devices/PrawnDO/blacs_tabs.py @@ -29,6 +29,12 @@ def initialise_GUI(self): self.supports_remote_value_check(True) self.supports_smart_programming(True) + + def get_child_from_connection_table(self, parent_device_name, port): + # all child direct outputs are actually connected to the internal device _PrawnDigitalOutputs + # so we must look under that device for the port + return self.connection_table.find_child(f'{self.device_name:s}__pod', port) + def initialise_workers(self): self.create_worker( From 4b81e7095c6293d9b9ed3b526e99b7850f18ec99 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Wed, 14 Aug 2024 15:52:41 -0400 Subject: [PATCH 06/10] Ensure `initial_trigger_time` is not modified when using the internal IntermediateDevice with a direct clockline trigger. --- .../PrawnDO/labscript_devices.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/labscript_devices/PrawnDO/labscript_devices.py b/labscript_devices/PrawnDO/labscript_devices.py index 514da64..29507a4 100644 --- a/labscript_devices/PrawnDO/labscript_devices.py +++ b/labscript_devices/PrawnDO/labscript_devices.py @@ -215,6 +215,25 @@ def __init__(self, name, self.BLACS_connection = com_port + self._initial_trigger_time = 0 + + # following three defs ensure initial_trigger_time is not modified + # when directly triggered from a clockline using an internal IntermediateDevice + @property + def initial_trigger_time(self): + return self._initial_trigger_time + + @initial_trigger_time.setter + def initial_trigger_time(self, value): + if value != 0 and hasattr(self, "__intermediate"): + raise LabscriptError("You cannot set the initial trigger time when the PrawnDO is directly triggered by a clockline") + self._initial_trigger_time = value + + def set_initial_trigger_time(self, *args, **kwargs): + if hasattr(self, "__intermediate"): + raise LabscriptError("You cannot set the initial trigger time when the PrawnDO is directly triggered by a clockline") + return super().set_initial_trigger_time(*args, **kwargs) + def add_device(self, device): if isinstance(device, _PrawnDOPseudoclock): From f2601a9e791cecbab86d883a2b4567e6c3538c1f Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 20 Aug 2024 15:06:56 -0400 Subject: [PATCH 07/10] Solidify hardware channel specification to `'doX'`. This allows for correct auto widget creation. It also move specification out of hex to to decimal, matching the labeling of the GPIO pins. --- docs/source/devices/prawndo.rst | 21 +++++++++---------- labscript_devices/PrawnDO/blacs_tabs.py | 7 +++++-- labscript_devices/PrawnDO/blacs_workers.py | 4 ++-- .../PrawnDO/labscript_devices.py | 11 ++++------ 4 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/source/devices/prawndo.rst b/docs/source/devices/prawndo.rst index e162e74..359d3b0 100644 --- a/docs/source/devices/prawndo.rst +++ b/docs/source/devices/prawndo.rst @@ -45,16 +45,15 @@ Usage The pinout for the PrawnDO is as follows: -* Outputs 0-15 (labelled by default in hex 0-F): GPIO pins 0-15, respectively. +* Outputs 0-15: GPIO pins 0-15, respectively. * External Trigger input: GPIO 16 * External Clock input: GPIO 20 Note that signal cables should be connected to the Pico digital grounds for proper operation. The PrawnDO can provide up to 16 digital outputs, which are accessed via `name.outputs`. -Each channel is specified using the corresponding hex character (spanning 0-F for 0-15). -The channel string must end with a single character between 0-F to be valid -(i.e. `'flag 0'`, `'do 0'`, and `'0'` are all valid channel specifications for GPIO 0 of the PrawnDO). +Each channel is specified using with a string of the form `'doD'`, where `'D'` is the GPIO number +(i.e. `'do10'`, is the specification for GPIO 10 of the PrawnDO). An example connection table that uses the PrawnBlaster and PrawnDO: @@ -69,9 +68,9 @@ An example connection table that uses the PrawnBlaster and PrawnDO: PrawnDO(name='prawn_do', com_port='COM5', clock_line=prawn.clocklines[0]) - DigitalOut('do0', prawn_do.outputs, 'flag 0') - DigitalOut('do1', prawn_do.outputs, 'chan 1') - DigitalOut('do10', prawn_do.outputs, 'flag C') + DigitalOut('do0', prawn_do.outputs, 'do0') + DigitalOut('do1', prawn_do.outputs, 'do1') + DigitalOut('do12', prawn_do.outputs, 'do12') if __name__ == "__main__": @@ -138,9 +137,9 @@ An example connection table using external clocks with the default frequency of PrawnDO(name='prawn_do', com_port='COM5', clock_line=prawn.clocklines[0], external_clock=True) - DigitalOut('do0', prawn_do.outputs, 'flag 0') - DigitalOut('do1', prawn_do.outputs, 'chan 1') - DigitalOut('do10', prawn_do.outputs, 'flag C') + DigitalOut('do0', prawn_do.outputs, 'do0') + DigitalOut('do1', prawn_do.outputs, 'do1') + DigitalOut('do12', prawn_do.outputs, 'do12') if __name__ == "__main__": @@ -172,7 +171,7 @@ the second instruction (`do0.go_low(t)`) must be at least 5 clock cycles after t .. code-block:: python t = 0 - do.go_high(t) + do0.go_high(t) t = 1e-3 wait('my_wait', t) do0.go_low(t) diff --git a/labscript_devices/PrawnDO/blacs_tabs.py b/labscript_devices/PrawnDO/blacs_tabs.py index 547cdac..069017d 100644 --- a/labscript_devices/PrawnDO/blacs_tabs.py +++ b/labscript_devices/PrawnDO/blacs_tabs.py @@ -16,11 +16,14 @@ class PrawnDOTab(DeviceTab): def initialise_GUI(self): do_prop = {} for i in range(0, 16): - do_prop['0x{:01X}'.format(i)] = {} + do_prop['do{:01d}'.format(i)] = {} self.create_digital_outputs(do_prop) + def sort(channel): + return int(channel.split('do')[-1]) + _, _, do_widgets = self.auto_create_widgets() - self.auto_place_widgets(do_widgets) + self.auto_place_widgets(('Digital Outputs', do_widgets, sort)) device = self.settings['connection_table'].find_by_name(self.device_name) diff --git a/labscript_devices/PrawnDO/blacs_workers.py b/labscript_devices/PrawnDO/blacs_workers.py index e28f54d..27bd787 100644 --- a/labscript_devices/PrawnDO/blacs_workers.py +++ b/labscript_devices/PrawnDO/blacs_workers.py @@ -173,7 +173,7 @@ def _dict_to_int(self, d): """ val = 0 for conn, value in d.items(): - val |= value << int(conn, 16) + val |= value << int(conn) return val @@ -186,7 +186,7 @@ def _int_to_dict(self, val): Returns: dict: Dictonary with output channels as keys and values are boolean states """ - return {f'0x{i:X}':((val >> i) & 1) for i in range(16)} + return {f'do{i:d}':((val >> i) & 1) for i in range(16)} def check_status(self): '''Checks operational status of the PrawnDO. diff --git a/labscript_devices/PrawnDO/labscript_devices.py b/labscript_devices/PrawnDO/labscript_devices.py index 29507a4..1019f69 100644 --- a/labscript_devices/PrawnDO/labscript_devices.py +++ b/labscript_devices/PrawnDO/labscript_devices.py @@ -58,10 +58,7 @@ def add_device(self, device): class _PrawnDigitalOutputs(IntermediateDevice): allowed_children = [DigitalOut] - allowed_channels = ('0', '1', '2', '3', - '4', '5', '6', '7', - '8', '9', 'A', 'B', - 'C', 'D', 'E', 'F') + allowed_channels = tuple(range(16)) def __init__(self, name, parent_device, **kwargs): @@ -88,7 +85,7 @@ def add_device(self, device): """ conn = device.connection - chan = conn.split(' ')[-1] + chan = int(conn.split('do')[-1]) if chan not in self.allowed_channels: raise LabscriptError(f'Invalid channel specification: {conn}') @@ -268,8 +265,8 @@ def generate_code(self, hdf5_file): # as the output word for the pins for output in outputs: output.make_timeseries(times) - chan = output.connection.split(' ')[-1] - bits[int(chan, 16)] = np.asarray(output.timeseries, dtype = np.uint16) + chan = int(output.connection.split('do')[-1]) + bits[chan] = np.asarray(output.timeseries, dtype = np.uint16) # Merge list of lists into an array with a single 16 bit integer column bit_sets = np.array(bitfield(bits, dtype=np.uint16)) From 52cbbe9a42c194134c93d22604fe30c45f9b8519 Mon Sep 17 00:00:00 2001 From: David Meyer Date: Fri, 30 Aug 2024 22:41:49 -0400 Subject: [PATCH 08/10] Fix issues in `doXX` channel changeover --- labscript_devices/PrawnDO/blacs_workers.py | 2 +- labscript_devices/PrawnDO/labscript_devices.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/labscript_devices/PrawnDO/blacs_workers.py b/labscript_devices/PrawnDO/blacs_workers.py index 27bd787..d50cac9 100644 --- a/labscript_devices/PrawnDO/blacs_workers.py +++ b/labscript_devices/PrawnDO/blacs_workers.py @@ -173,7 +173,7 @@ def _dict_to_int(self, d): """ val = 0 for conn, value in d.items(): - val |= value << int(conn) + val |= value << int(conn.split('do')[-1]) return val diff --git a/labscript_devices/PrawnDO/labscript_devices.py b/labscript_devices/PrawnDO/labscript_devices.py index 1019f69..88dde4f 100644 --- a/labscript_devices/PrawnDO/labscript_devices.py +++ b/labscript_devices/PrawnDO/labscript_devices.py @@ -80,8 +80,7 @@ def add_device(self, device): Args: device (): Device to attach. Must be a digital output. - Allowed connections are a string that ends with a 0-F hex - channel number. + Allowed connections are a string of the form `doXX` """ conn = device.connection From adc11980a3f9886422341257363ac6f04636c49c Mon Sep 17 00:00:00 2001 From: David Meyer Date: Tue, 3 Sep 2024 14:16:49 -0400 Subject: [PATCH 09/10] Add PrawnDO to project README as supported. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 88e4b94..ff27e41 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ The following devices have been implemented in the _labscript suite_:† Date: Tue, 3 Sep 2024 14:26:25 -0400 Subject: [PATCH 10/10] Update stale URLs in the docs --- docs/source/devices/prawnblaster.rst | 2 +- docs/source/devices/prawndo.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/devices/prawnblaster.rst b/docs/source/devices/prawnblaster.rst index 8d19473..4393ef1 100644 --- a/docs/source/devices/prawnblaster.rst +++ b/docs/source/devices/prawnblaster.rst @@ -89,7 +89,7 @@ it is often a good idea to add unity-gain channel buffers to the inputs and outp Using buffers and line drivers from a LVCMOS family with 5V/TTL tolerant inputs can provide compatibility with TTL inputs and drive higher capacitance loads (such a long BNC cables) more reliably. Examples that implement these buffers can be found `here `_ -and `here `_. +and `here `_. Detailed Documentation diff --git a/docs/source/devices/prawndo.rst b/docs/source/devices/prawndo.rst index 359d3b0..fd051ac 100644 --- a/docs/source/devices/prawndo.rst +++ b/docs/source/devices/prawndo.rst @@ -155,7 +155,7 @@ While the PrawnBlaster and PrawnDO boards can be used as is, it is often a good idea to add unity-gain channel buffers to the inputs and outputs. Using buffers and line drivers from a LVCMOS family with 5V/TTL tolerant inputs can provide compatibility with TTL inputs and drive higher capacitance loads (such a long BNC cables) more reliably. -An example that implements these buffers can be found `here `_. +An example that implements these buffers can be found `here `_. Waits -----