Skip to content

Commit c78247b

Browse files
authored
Merge pull request #132 from Json-To-String/prawn-device-updates
Prawn device updates
2 parents ce3506b + fa5ff21 commit c78247b

File tree

4 files changed

+171
-57
lines changed

4 files changed

+171
-57
lines changed

labscript_devices/PrawnBlaster/blacs_workers.py

Lines changed: 99 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import h5py
1616
import numpy as np
1717
from blacs.tab_base_classes import Worker
18+
from labscript import LabscriptError
1819
from labscript_utils.connections import _ensure_str
1920
import labscript_utils.properties as properties
2021

@@ -56,28 +57,85 @@ def init(self):
5657
self.wait_timeout = None
5758
self.h5_file = None
5859
self.started = False
59-
60-
self.prawnblaster = serial.Serial(self.com_port, 115200, timeout=1)
60+
self.min_version = (1, 1, 0)
61+
62+
self.conn = serial.Serial(self.com_port, 115200, timeout=1)
6163
self.check_status()
6264

6365
# configure number of pseudoclocks
64-
self.prawnblaster.write(b"setnumpseudoclocks %d\r\n" % self.num_pseudoclocks)
65-
assert self.prawnblaster.readline().decode() == "ok\r\n"
66+
self.send_command_ok(f"setnumpseudoclocks {self.num_pseudoclocks}")
6667

6768
# Configure pins
6869
for i, (out_pin, in_pin) in enumerate(zip(self.out_pins, self.in_pins)):
69-
self.prawnblaster.write(b"setoutpin %d %d\r\n" % (i, out_pin))
70-
assert self.prawnblaster.readline().decode() == "ok\r\n"
71-
self.prawnblaster.write(b"setinpin %d %d\r\n" % (i, in_pin))
72-
assert self.prawnblaster.readline().decode() == "ok\r\n"
70+
self.send_command_ok(f"setoutpin {i} {out_pin}")
71+
self.send_command_ok(f"setinpin {i} {in_pin}")
7372

74-
# Check if fast serial is available
7573
version, _ = self.get_version()
76-
self.fast_serial = version >= (1, 1, 0)
74+
print(f'Connected to version: {version}')
75+
assert version >= self.min_version, f'Incompatible firmware, must be >= {self.min_version}'
76+
77+
if version >= (1, 2, 0):
78+
board = self.get_board()
79+
else:
80+
board = 'pico1'
81+
print(f'Version {version} too low to use pico2 firmware, consider upgrading firmware')
82+
83+
print(f'Connected to board: {board}')
84+
if board.strip() != self.pico_board.strip():
85+
raise LabscriptError(f'firmware reports {board} attached, labscript expects {self.pico_board}')
86+
87+
current_status = self.read_status()
88+
print(f'Current status is {current_status}')
89+
90+
def _read_full_buffer(self):
91+
'''Used to get any extra lines from device after a failed send_command'''
92+
93+
resp = self.conn.readlines()
94+
str_resp = ''.join([st.decode() for st in resp])
95+
96+
return str_resp
97+
98+
def send_command(self, command, readlines=False):
99+
'''Sends the supplied string command and checks for a response.
100+
101+
Automatically applies the correct termination characters.
102+
103+
Args:
104+
command (str): Command to send. Termination and encoding is done automatically.
105+
readlines (bool, optional): Use pyserial's readlines functionality to read multiple
106+
response lines. Slower as it relies on timeout to terminate reading.
107+
108+
Returns:
109+
str: String response from the PrawnBlaster
110+
'''
111+
command += '\r\n'
112+
self.conn.write(command.encode())
113+
114+
if readlines:
115+
str_resp = self._read_full_buffer()
116+
else:
117+
str_resp = self.conn.readline().decode()
77118

119+
return str_resp
120+
121+
def send_command_ok(self, command):
122+
'''Sends the supplied string command and confirms 'ok' response.
123+
124+
Args:
125+
command (str): String command to send.
126+
127+
Raises:
128+
LabscriptError: If response is not `ok\\r\\n`
129+
'''
130+
131+
resp = self.send_command(command)
132+
if resp != 'ok\r\n':
133+
# get complete error message
134+
resp += self._read_full_buffer()
135+
raise LabscriptError(f"Command '{command:s}' failed. Got response '{repr(resp)}'")
136+
78137
def get_version(self):
79-
self.prawnblaster.write(b"version\r\n")
80-
version_str = self.prawnblaster.readline().decode()
138+
version_str = self.send_command('version', readlines=True)
81139
assert version_str.startswith("version: ")
82140
version = version_str[9:].strip()
83141

@@ -91,6 +149,17 @@ def get_version(self):
91149
assert len(version) == 3
92150

93151
return version, overclock
152+
153+
def get_board(self):
154+
'''Responds with pico board version.
155+
156+
Returns:
157+
(str): Either "pico1" for a Pi Pico 1 board or "pico2" for a Pi Pico 2 board.'''
158+
resp = self.send_command('board')
159+
assert resp.startswith('board:'), f'Board command failed, got: {resp}'
160+
pico_str = resp.split(':')[-1].strip()
161+
162+
return pico_str
94163

95164
def check_status(self):
96165
"""Checks the operational status of the PrawnBlaster.
@@ -128,8 +197,7 @@ def check_status(self):
128197
):
129198
# Try to read out wait. For now, we're only reading out waits from
130199
# pseudoclock 0 since they should all be the same (requirement imposed by labscript)
131-
self.prawnblaster.write(b"getwait %d %d\r\n" % (0, self.current_wait))
132-
response = self.prawnblaster.readline().decode()
200+
response = self.send_command(f'getwait {0}, {self.current_wait}')
133201
if response != "wait not yet available\r\n":
134202
# Parse the response from the PrawnBlaster
135203
wait_remaining = int(response)
@@ -202,8 +270,7 @@ def read_status(self):
202270
- **clock-status** (int): Clock status code
203271
"""
204272

205-
self.prawnblaster.write(b"status\r\n")
206-
response = self.prawnblaster.readline().decode()
273+
response = self.send_command("status", readlines=True)
207274
match = re.match(r"run-status:(\d) clock-status:(\d)(\r\n)?", response)
208275
if match:
209276
return int(match.group(1)), int(match.group(2))
@@ -231,11 +298,9 @@ def program_manual(self, values):
231298
pin = int(channel.split()[1])
232299
pseudoclock = self.out_pins.index(pin)
233300
if value:
234-
self.prawnblaster.write(b"go high %d\r\n" % pseudoclock)
301+
self.send_command_ok(f"go high {pseudoclock}")
235302
else:
236-
self.prawnblaster.write(b"go low %d\r\n" % pseudoclock)
237-
238-
assert self.prawnblaster.readline().decode() == "ok\r\n"
303+
self.send_command_ok(f"go low {pseudoclock}")
239304

240305
return values
241306

@@ -309,15 +374,13 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
309374
clock_frequency = self.device_properties["clock_frequency"]
310375

311376
# Now set the clock details
312-
self.prawnblaster.write(b"setclock %d %d\r\n" % (clock_mode, clock_frequency))
313-
response = self.prawnblaster.readline().decode()
314-
assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'"
377+
response = self.send_command_ok(f"setclock {clock_mode} {clock_frequency}")
315378

316379
# Program instructions
317380
for pseudoclock, pulse_program in enumerate(pulse_programs):
318381
total_inst = len(pulse_program)
319382
# check if it is more efficient to fully refresh
320-
if not fresh and self.smart_cache[pseudoclock] is not None and self.fast_serial:
383+
if not fresh and self.smart_cache[pseudoclock] is not None:
321384
# get more convenient handles to smart cache arrays
322385
curr_inst = self.smart_cache[pseudoclock]
323386

@@ -337,17 +400,17 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
337400
if new_inst / total_inst > 0.1:
338401
fresh = True
339402

340-
if (fresh or self.smart_cache[pseudoclock] is None) and self.fast_serial:
403+
if (fresh or self.smart_cache[pseudoclock] is None):
341404
print('binary programming')
342-
self.prawnblaster.write(b"setb %d %d %d\r\n" % (pseudoclock, 0, len(pulse_program)))
343-
response = self.prawnblaster.readline().decode()
405+
self.conn.write(b"setb %d %d %d\r\n" % (pseudoclock, 0, len(pulse_program)))
406+
response = self.conn.readline().decode()
344407
assert (
345408
response == "ready\r\n"
346409
), f"PrawnBlaster said '{response}', expected 'ready'"
347410
program_array = np.array([pulse_program['half_period'],
348411
pulse_program['reps']], dtype='<u4').T
349-
self.prawnblaster.write(program_array.tobytes())
350-
response = self.prawnblaster.readline().decode()
412+
self.conn.write(program_array.tobytes())
413+
response = self.conn.readline().decode()
351414
assert (
352415
response == "ok\r\n"
353416
), f"PrawnBlaster said '{response}', expected 'ok'"
@@ -361,7 +424,7 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
361424

362425
# Only program instructions that differ from what's in the smart cache:
363426
if self.smart_cache[pseudoclock][i] != instruction:
364-
self.prawnblaster.write(
427+
self.conn.write(
365428
b"set %d %d %d %d\r\n"
366429
% (
367430
pseudoclock,
@@ -370,7 +433,7 @@ def transition_to_buffered(self, device_name, h5file, initial_values, fresh):
370433
instruction["reps"],
371434
)
372435
)
373-
response = self.prawnblaster.readline().decode()
436+
response = self.conn.readline().decode()
374437
assert (
375438
response == "ok\r\n"
376439
), f"PrawnBlaster said '{response}', expected 'ok'"
@@ -392,9 +455,7 @@ def start_run(self):
392455

393456
# Start in software:
394457
self.logger.info("sending start")
395-
self.prawnblaster.write(b"start\r\n")
396-
response = self.prawnblaster.readline().decode()
397-
assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'"
458+
self.send_command_ok("start")
398459

399460
# set started = True
400461
self.started = True
@@ -405,9 +466,7 @@ def wait_for_trigger(self):
405466

406467
# Set to wait for trigger:
407468
self.logger.info("sending hwstart")
408-
self.prawnblaster.write(b"hwstart\r\n")
409-
response = self.prawnblaster.readline().decode()
410-
assert response == "ok\r\n", f"PrawnBlaster said '{response}', expected 'ok'"
469+
self.send_command_ok("hwstart")
411470

412471
running = False
413472
while not running:
@@ -477,7 +536,7 @@ def transition_to_manual(self):
477536
def shutdown(self):
478537
"""Cleanly shuts down the connection to the PrawnBlaster hardware."""
479538

480-
self.prawnblaster.close()
539+
self.conn.close()
481540

482541
def abort_buffered(self):
483542
"""Aborts a currently running buffered execution.
@@ -489,8 +548,8 @@ def abort_buffered(self):
489548
# Only need to send abort signal if we have told the PrawnBlaster to wait
490549
# for a hardware trigger. Otherwise it's just been programmed with
491550
# instructions and there is nothing we need to do to abort.
492-
self.prawnblaster.write(b"abort\r\n")
493-
assert self.prawnblaster.readline().decode() == "ok\r\n"
551+
self.conn.write(b"abort\r\n")
552+
assert self.conn.readline().decode() == "ok\r\n"
494553
# loop until abort complete
495554
while self.read_status()[0] != 5:
496555
time.sleep(0.5)

labscript_devices/PrawnBlaster/labscript_devices.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,16 @@ class PrawnBlaster(PseudoclockDevice):
144144
"""Minimum required length of a wait before retrigger can be detected.
145145
Corresponds to 4 instructions."""
146146
allowed_children = [_PrawnBlasterPseudoclock, _PrawnBlasterDummyPseudoclock]
147-
max_instructions = 30000
148-
"""Maximum numaber of instructions per pseudoclock. Max is 30,000 for a single
149-
pseudoclock."""
147+
allowed_boards = ['pico1', 'pico2']
148+
max_instructions_map = {'pico1' : 30000, 'pico2' : 60000}
149+
"""Maximum number of instructions per pseudoclock for each pico board. """
150+
max_frequency_map = {'pico1' : 133e6, 'pico2' : 150e6}
150151

151152
@set_passed_properties(
152153
property_names={
153154
"connection_table_properties": [
154155
"com_port",
156+
'pico_board',
155157
"in_pins",
156158
"out_pins",
157159
"num_pseudoclocks",
@@ -175,6 +177,7 @@ def __init__(
175177
trigger_device=None,
176178
trigger_connection=None,
177179
com_port="COM1",
180+
pico_board = 'pico1',
178181
num_pseudoclocks=1,
179182
out_pins=None,
180183
in_pins=None,
@@ -191,6 +194,7 @@ def __init__(
191194
name (str): python variable name to assign to the PrawnBlaster
192195
com_port (str): COM port assigned to the PrawnBlaster by the OS. Takes
193196
the form of `'COMd'`, where `d` is an integer.
197+
pico_board (str): The version of pico board used, pico1 or pico2.
194198
num_pseudoclocks (int): Number of pseudoclocks to create. Ranges from 1-4.
195199
trigger_device (:class:`~labscript.IntermediateDevice`, optional): Device
196200
that will send the hardware start trigger when using the PrawnBlaster
@@ -203,8 +207,8 @@ def __init__(
203207
triggering. Must have length of at least `num_pseudoclocks`.
204208
Defaults to `[0,0,0,0]`
205209
clock_frequency (float, optional): Frequency of clock. Standard range
206-
accepts up to 133 MHz. An experimental overclocked firmware is
207-
available that allows higher frequencies.
210+
accepts up to 133 MHz for pico1, 150 MHz for pico2. An experimental overclocked
211+
firmware is available that allows higher frequencies.
208212
external_clock_pin (int, optional): If not `None` (the default),
209213
the PrawnBlaster uses an external clock on the provided pin. Valid
210214
options are `20` and `22`. The external frequency must be defined
@@ -214,14 +218,26 @@ def __init__(
214218
215219
"""
216220

221+
if pico_board in self.allowed_boards:
222+
self.pico_board = pico_board
223+
else:
224+
raise LabscriptError(f'Pico board specified not in {self.allowed_boards}')
225+
217226
# Check number of pseudoclocks is within range
218227
if num_pseudoclocks < 1 or num_pseudoclocks > 4:
219228
raise LabscriptError(
220229
f"The PrawnBlaster {name} only supports between 1 and 4 pseudoclocks"
221230
)
222-
231+
232+
# Set max instructions based on board
233+
self.max_instructions = self.max_instructions_map[self.pico_board]
223234
# Update the specs based on the number of pseudoclocks
224235
self.max_instructions = self.max_instructions // num_pseudoclocks
236+
237+
self.max_frequency = self.max_frequency_map[self.pico_board]
238+
239+
if clock_frequency > self.max_frequency:
240+
raise ValueError(f'Clock frequency must be less than {int(self.max_frequency * 10**-6)} MHz')
225241
# Update the specs based on the clock frequency
226242
if self.clock_resolution != 2 / clock_frequency:
227243
factor = (2 / clock_frequency) / self.clock_resolution

0 commit comments

Comments
 (0)