Skip to content

Commit

Permalink
Initial commit of support for multi-pseudoclock PrawnBlaster.
Browse files Browse the repository at this point in the history
I have not yet fully implemented waits in the blacs_worker yet!
  • Loading branch information
philipstarkey committed Feb 28, 2021
1 parent bdb8c13 commit 74ce6ad
Show file tree
Hide file tree
Showing 6 changed files with 830 additions and 0 deletions.
19 changes: 19 additions & 0 deletions labscript_devices/PrawnBlaster/__init__.py
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"
)
127 changes: 127 additions & 0 deletions labscript_devices/PrawnBlaster/blacs_tabs.py
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)
206 changes: 206 additions & 0 deletions labscript_devices/PrawnBlaster/blacs_workers.py
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()
Loading

0 comments on commit 74ce6ad

Please sign in to comment.