Skip to content

Commit 74ce6ad

Browse files
committed
Initial commit of support for multi-pseudoclock PrawnBlaster.
I have not yet fully implemented waits in the blacs_worker yet!
1 parent bdb8c13 commit 74ce6ad

File tree

6 files changed

+830
-0
lines changed

6 files changed

+830
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/PrawnBlaster/__init__.py #
4+
# #
5+
# Copyright 2021, Philip Starkey #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
from labscript_devices import deprecated_import_alias
14+
15+
16+
# For backwards compatibility with old experiment scripts:
17+
PrawnBlaster = deprecated_import_alias(
18+
"labscript_devices.PrawnBlaster.labscript_devices.PrawnBlaster"
19+
)
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/PrawnBlaster/blacs_tab.py #
4+
# #
5+
# Copyright 2021, Philip Starkey #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
from blacs.device_base_class import (
14+
DeviceTab,
15+
define_state,
16+
MODE_BUFFERED,
17+
MODE_MANUAL,
18+
MODE_TRANSITION_TO_BUFFERED,
19+
MODE_TRANSITION_TO_MANUAL,
20+
)
21+
import labscript_utils.properties
22+
23+
from qtutils.qt import QtWidgets
24+
25+
26+
class PrawnBlasterTab(DeviceTab):
27+
def initialise_GUI(self):
28+
self.connection_table_properties = (
29+
self.settings["connection_table"].find_by_name(self.device_name).properties
30+
)
31+
32+
digital_outs = {}
33+
for pin in self.connection_table_properties["out_pins"]:
34+
digital_outs[f"GPIO {pin:02d}"] = {}
35+
36+
# Create a single digital output
37+
self.create_digital_outputs(digital_outs)
38+
# Create widgets for output objects
39+
_, _, do_widgets = self.auto_create_widgets()
40+
# and auto place the widgets in the UI
41+
self.auto_place_widgets(("Flags", do_widgets))
42+
43+
# Create status labels
44+
self.status_label = QtWidgets.QLabel("Status: Unknown")
45+
self.clock_status_label = QtWidgets.QLabel("Clock status: Unknown")
46+
self.get_tab_layout().addWidget(self.status_label)
47+
self.get_tab_layout().addWidget(self.clock_status_label)
48+
49+
# Set the capabilities of this device
50+
self.supports_smart_programming(True)
51+
52+
# Create status monitor timout
53+
self.statemachine_timeout_add(2000, self.status_monitor)
54+
55+
def get_child_from_connection_table(self, parent_device_name, port):
56+
# Pass down channel name search to the pseudoclocks (so we can find the
57+
# clocklines)
58+
if parent_device_name == self.device_name:
59+
device = self.connection_table.find_by_name(self.device_name)
60+
61+
for pseudoclock_name, pseudoclock in device.child_list.items():
62+
for child_name, child in pseudoclock.child_list.items():
63+
# store a reference to the internal clockline
64+
if child.parent_port == port:
65+
return DeviceTab.get_child_from_connection_table(
66+
self, pseudoclock.name, port
67+
)
68+
69+
return None
70+
71+
def initialise_workers(self):
72+
# Find the COM port to be used
73+
com_port = str(
74+
self.settings["connection_table"]
75+
.find_by_name(self.device_name)
76+
.BLACS_connection
77+
)
78+
79+
worker_initialisation_kwargs = {
80+
"com_port": com_port,
81+
"num_pseudoclocks": self.connection_table_properties["num_pseudoclocks"],
82+
"out_pins": self.connection_table_properties["out_pins"],
83+
"in_pins": self.connection_table_properties["in_pins"],
84+
}
85+
self.create_worker(
86+
"main_worker",
87+
"labscript_devices.PrawnBlaster.blacs_workers.PrawnBlasterWorker",
88+
worker_initialisation_kwargs,
89+
)
90+
self.primary_worker = "main_worker"
91+
92+
@define_state(
93+
MODE_MANUAL
94+
| MODE_BUFFERED
95+
| MODE_TRANSITION_TO_BUFFERED
96+
| MODE_TRANSITION_TO_MANUAL,
97+
True,
98+
)
99+
def status_monitor(self, notify_queue=None):
100+
# When called with a queue, this function writes to the queue
101+
# when the pulseblaster is waiting. This indicates the end of
102+
# an experimental run.
103+
status, clock_status, waits_pending = yield (
104+
self.queue_work(self.primary_worker, "check_status")
105+
)
106+
107+
# Manual mode or aborted
108+
done_condition = status == 0 or status == 5
109+
110+
# Update GUI status/clock status widgets
111+
self.status_label.setText(f"Status: {status}")
112+
self.clock_status_label.setText(f"Clock status: {clock_status}")
113+
114+
if notify_queue is not None and done_condition and not waits_pending:
115+
# Experiment is over. Tell the queue manager about it, then
116+
# set the status checking timeout back to every 2 seconds
117+
# with no queue.
118+
notify_queue.put("done")
119+
self.statemachine_timeout_remove(self.status_monitor)
120+
self.statemachine_timeout_add(2000, self.status_monitor)
121+
122+
@define_state(MODE_BUFFERED, True)
123+
def start_run(self, notify_queue):
124+
self.statemachine_timeout_remove(self.status_monitor)
125+
yield (self.queue_work(self.primary_worker, "start_run"))
126+
self.status_monitor()
127+
self.statemachine_timeout_add(100, self.status_monitor, notify_queue)
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/PrawnBlaster/blacs_worker.py #
4+
# #
5+
# Copyright 2021, Philip Starkey #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
import time
14+
import labscript_utils.h5_lock
15+
import h5py
16+
from blacs.tab_base_classes import Worker
17+
import labscript_utils.properties as properties
18+
19+
20+
class PrawnBlasterWorker(Worker):
21+
def init(self):
22+
# fmt: off
23+
global h5py; import labscript_utils.h5_lock, h5py
24+
global serial; import serial
25+
global time; import time
26+
global re; import re
27+
self.smart_cache = {}
28+
self.cached_pll_params = {}
29+
# fmt: on
30+
31+
self.prawnblaster = serial.Serial(self.com_port, 115200, timeout=1)
32+
self.check_status()
33+
34+
# configure number of pseudoclocks
35+
self.prawnblaster.write(b"setnumpseudoclocks %d\r\n" % self.num_pseudoclocks)
36+
assert self.prawnblaster.readline().decode() == "ok\r\n"
37+
38+
# Configure pins
39+
for i, (out_pin, in_pin) in enumerate(zip(self.out_pins, self.in_pins)):
40+
self.prawnblaster.write(b"setoutpin %d %d\r\n" % (i, out_pin))
41+
assert self.prawnblaster.readline().decode() == "ok\r\n"
42+
self.prawnblaster.write(b"setinpin %d %d\r\n" % (i, in_pin))
43+
assert self.prawnblaster.readline().decode() == "ok\r\n"
44+
45+
def check_status(self):
46+
self.prawnblaster.write(b"status\r\n")
47+
response = self.prawnblaster.readline().decode()
48+
match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response)
49+
if match:
50+
return int(match.group(1)), int(match.group(2)), False
51+
elif response:
52+
raise Exception(
53+
f"PrawnBlaster is confused: saying '{response}' instead of 'run-status:<int> clock-status:<int>'"
54+
)
55+
else:
56+
raise Exception(
57+
f"PrawnBlaster is returning a invalid status '{response}'. Maybe it needs a reboot."
58+
)
59+
60+
def program_manual(self, values):
61+
for channel, value in values.items():
62+
pin = int(channel.split()[1])
63+
pseudoclock = self.out_pins.index(pin)
64+
if value:
65+
self.prawnblaster.write(b"go high %d\r\n" % pseudoclock)
66+
else:
67+
self.prawnblaster.write(b"go low %d\r\n" % pseudoclock)
68+
69+
assert self.prawnblaster.readline().decode() == "ok\r\n"
70+
71+
return values
72+
73+
def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
74+
if fresh:
75+
self.smart_cache = {}
76+
77+
# Get data from HDF5 file
78+
pulse_programs = []
79+
with h5py.File(h5file, "r") as hdf5_file:
80+
group = hdf5_file[f"devices/{device_name}"]
81+
for i in range(self.num_pseudoclocks):
82+
pulse_programs.append(group[f"PULSE_PROGRAM_{i}"][:])
83+
self.smart_cache.setdefault(i, [])
84+
device_properties = labscript_utils.properties.get(
85+
hdf5_file, device_name, "device_properties"
86+
)
87+
self.is_master_pseudoclock = device_properties["is_master_pseudoclock"]
88+
89+
# TODO: Configure clock from device properties
90+
clock_mode = 0
91+
clock_vcofreq = 0
92+
clock_plldiv1 = 0
93+
clock_plldiv2 = 0
94+
if device_properties["external_clock_pin"] is not None:
95+
if device_properties["external_clock_pin"] == 20:
96+
clock_mode = 1
97+
elif device_properties["external_clock_pin"] == 22:
98+
clock_mode = 2
99+
else:
100+
raise RuntimeError(
101+
f"Invalid external clock pin {device_properties['external_clock_pin']}. Pin must be 20, 22 or None."
102+
)
103+
clock_frequency = device_properties["clock_frequency"]
104+
105+
if clock_mode == 0:
106+
if clock_frequency == 100e6:
107+
clock_vcofreq = 1200e6
108+
clock_plldiv1 = 6
109+
clock_plldiv2 = 2
110+
elif clock_frequency in self.cached_pll_params:
111+
pll_params = self.cached_pll_params[clock_frequency]
112+
clock_vcofreq = pll_params["vcofreq"]
113+
clock_plldiv1 = pll_params["plldiv1"]
114+
clock_plldiv2 = pll_params["plldiv2"]
115+
else:
116+
self.logger.info("Calculating PLL parameters...")
117+
osc_freq = 12e6
118+
# Techniclally FBDIV can be 16-320 (see 2.18.2 in
119+
# https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf )
120+
# however for a 12MHz reference clock, the range is smaller to ensure
121+
# vcofreq is between 400 and 1600 MHz.
122+
found = False
123+
for fbdiv in range(134, 33, -1):
124+
vcofreq = osc_freq * fbdiv
125+
# PLL1 div should be greater than pll2 div if possible so we start high
126+
for pll1 in range(7, 0, -1):
127+
for pll2 in range(1, 8):
128+
if vco_freq / (pll1 * pll2) == clock_frequency:
129+
found = True
130+
clock_vcofreq = vcofreq
131+
clock_plldiv1 = pll1
132+
clock_plldiv2 = pll2
133+
pll_params = {}
134+
pll_params["vcofreq"] = clock_vcofreq
135+
pll_params["plldiv1"] = clock_plldiv1
136+
pll_params["plldiv2"] = clock_plldiv2
137+
self.cached_pll_params[clock_frequency] = pll_params
138+
break
139+
if found:
140+
break
141+
if found:
142+
break
143+
if not found:
144+
raise RuntimeError(
145+
"Could not determine appropriate clock paramaters"
146+
)
147+
148+
# Now set the clock details
149+
self.prawnblaster.write(
150+
b"setclock %d %d %d %d %d\r\n"
151+
% (clock_mode, clock_frequency, clock_vcofreq, clock_plldiv1, clock_plldiv2)
152+
)
153+
response = self.prawnblaster.readline().decode()
154+
assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'"
155+
156+
# TODO: Save any information we need for wait monitor
157+
158+
# Program instructions
159+
for pseudoclock, pulse_program in enumerate(pulse_programs):
160+
for i, instruction in enumerate(pulse_program):
161+
if i == len(self.smart_cache[pseudoclock]):
162+
# Pad the smart cache out to be as long as the program:
163+
self.smart_cache[pseudoclock].append(None)
164+
165+
# Only program instructions that differ from what's in the smart cache:
166+
if self.smart_cache[pseudoclock][i] != instruction:
167+
self.prawnblaster.write(
168+
b"set %d %d %d %d\r\n"
169+
% (pseudoclock, i, instruction["period"], instruction["reps"])
170+
)
171+
response = self.prawnblaster.readline().decode()
172+
assert (
173+
response == "ok\r\n"
174+
), f"PrawnBlaster said '{response}', expected 'ok'"
175+
self.smart_cache[pseudoclock][i] = instruction
176+
177+
# All outputs end on 0
178+
final = {}
179+
for pin in self.out_pins:
180+
final[f"GPIO {pin:02d}"] = 0
181+
return final
182+
183+
def start_run(self):
184+
# Start in software:
185+
self.logger.info("sending start")
186+
self.prawnblaster.write(b"start\r\n")
187+
response = self.prawnblaster.readline().decode()
188+
assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'"
189+
190+
def transition_to_manual(self):
191+
# TODO: write this
192+
return True
193+
194+
def shutdown(self):
195+
self.prawnblaster.close()
196+
197+
def abort_buffered(self):
198+
self.prawnblaster.write(b"abort\r\n")
199+
assert self.prawnblaster.readline().decode() == "ok\r\n"
200+
# loop until abort complete
201+
while self.check_status()[0] != 5:
202+
time.sleep(0.5)
203+
return True
204+
205+
def abort_transition_to_buffered(self):
206+
return self.abort_buffered()

0 commit comments

Comments
 (0)