-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit of support for multi-pseudoclock PrawnBlaster.
I have not yet fully implemented waits in the blacs_worker yet!
- Loading branch information
1 parent
bdb8c13
commit 74ce6ad
Showing
6 changed files
with
830 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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:<int> clock-status:<int>'" | ||
) | ||
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() |
Oops, something went wrong.