|
| 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