Newer
Older
glitch-o-bolt / scope.py
  1. import logging
  2. import serial
  3. from serial.tools.list_ports import comports
  4. from typing import Union, List
  5.  
  6. class ADCSettings():
  7. def __init__(self, dev:serial) -> None:
  8. self._dev = dev
  9. self._clk_freq = 25000000
  10. self._delay = 0
  11.  
  12. @property
  13. def clk_freq(self) -> int:
  14. """
  15. Get ADC CLK Frequency
  16. """
  17. return self._clk_freq
  18. @clk_freq.setter
  19. def clk_freq(self, freq:int) -> None:
  20. """
  21. Set ADC CLK Frequency, valid between 0.5kHz and 31.25MHz
  22. """
  23. BASE_CLK = 250000000
  24. pio_freq = freq*4
  25. divider = BASE_CLK/pio_freq
  26. integer = int(divider)
  27. frac = int((divider-integer)*256)
  28. if frac == 256:
  29. frac = 0
  30. integer += 1
  31. self._dev.write(f":ADC:PLL {integer},{frac}\n".encode("ascii"))
  32. self._clk_freq = BASE_CLK/(integer+frac/256)/4
  33. @property
  34. def delay(self) -> int:
  35. """
  36. Get delay between trigger and start of sampling in cycles (8.3ns)
  37. """
  38. return self._delay
  39. @delay.setter
  40. def delay(self, delay) -> None:
  41. """
  42. Set delay between trigger and start of sampling in cycles (8.3ns)
  43. """
  44. self._delay = delay
  45. self._dev.write(f":ADC:DELAY {int(delay)}\n".encode("ascii"))
  46.  
  47. class GlitchSettings():
  48. def __init__(self, dev:serial) -> None:
  49. self._dev = dev
  50. self._offset = 10
  51. self._repeat = 10
  52.  
  53. @property
  54. def ext_offset(self) -> int:
  55. """
  56. Delay between trigger and start of glitch in cycles (8.3ns)
  57. """
  58. return self._offset
  59. @ext_offset.setter
  60. def ext_offset(self, offset:int) -> None:
  61. """
  62. Set delay between trigger and start of glitch in cycles (8.3ns)
  63. """
  64. self._dev.write(f":GLITCH:DELAY {int(offset)}\n".encode("ascii"))
  65. self._offset = offset
  66. @property
  67. def repeat(self) -> int:
  68. """Width of glitch in cycles (approx = 8.3 ns * width)"""
  69. return self._repeat
  70.  
  71. @repeat.setter
  72. def repeat(self, width:int) -> None:
  73. """
  74. Set width of glitch in cycles (8.3ns)
  75. """
  76. self._dev.write(f":GLITCH:LEN {int(width)}\n".encode("ascii"))
  77. self._repeat = width
  78.  
  79. class GPIOSettings():
  80. def __init__(self, dev:serial) -> None:
  81. self.gpio = []
  82. for i in range(0, 4):
  83. self.gpio.append(list())
  84. self.dev = dev
  85. self.MAX_CHANGES = 255
  86. self.MAX_DELAY = 2147483647
  87. def add(self, pin:int, state:bool, delay:int=None, seconds:float=None) -> None:
  88. """
  89. Add state change to gpio
  90.  
  91. Arguments
  92. ---------
  93. pin : int
  94. Which pin to add state change to, [0,3]
  95. state : bool
  96. What the state of the pin should be
  97. delay : int
  98. Number of cycles delay after state change, each cycle is 8.3ns
  99. seconds : float
  100. Seconds of delay after state change if delay is not provided
  101.  
  102. Returns
  103. -------
  104. None
  105. """
  106. if pin < 0 or pin > 3:
  107. raise ValueError("Pin must be between 0 and 3")
  108. if len(self.gpio[pin]) >= self.MAX_CHANGES:
  109. raise ValueError("Pin reached max state changes")
  110.  
  111. if delay is None:
  112. if seconds is None:
  113. raise ValueError("delay or seconds must be provided")
  114. delay = int(seconds*100000000)
  115.  
  116. if delay > self.MAX_DELAY:
  117. raise ValueError("delay exceeds maximum")
  118. self.gpio[pin].append((delay << 1) | state)
  119. def reset(self) -> None:
  120. """
  121. Reset all GPIO state changes
  122.  
  123. Arguments
  124. ---------
  125. None
  126.  
  127. Returns
  128. -------
  129. None
  130. """
  131. self.dev.write(b":GPIO:RESET\n")
  132. for i in range(0, 4):
  133. self.gpio[i].clear()
  134.  
  135. def upload(self) -> None:
  136. """
  137. Upload GPIO changes to device
  138.  
  139. Arguments
  140. ---------
  141. None
  142.  
  143. Returns
  144. -------
  145. None
  146. """
  147. self.dev.write(b":GPIO:RESET\n")
  148. for i in range(0, 4):
  149. for item in self.gpio[i]:
  150. self.dev.write(f":GPIO:ADD {i},{item}\n".encode("ascii"))
  151.  
  152. class Scope():
  153. RISING_EDGE = 0
  154. FALLING_EDGE = 1
  155.  
  156. def __init__(self, port=None) -> None:
  157. if port is None:
  158. ports = comports()
  159. matches = [p.device for p in ports if p.interface == "Curious Bolt API"]
  160. if len(matches) != 1:
  161. matches = [p.device for p in ports if p.product == "Curious Bolt"]
  162. matches.sort()
  163. matches.reverse()
  164. if len(matches) != 2:
  165. raise IOError('Curious Bolt device not found. Please check if it\'s connected, and pass its port explicitly if it is.')
  166. port = matches[0]
  167.  
  168. self._port = port
  169. self._dev = serial.Serial(port, 115200*10, timeout=1.0)
  170. self._dev.reset_input_buffer()
  171. self._dev.write(b":VERSION?\n")
  172. data = self._dev.readline().strip()
  173. if data is None or data == b"":
  174. raise ValueError("Unable to connect")
  175. print(f"Connected to version: {data.decode('ascii')}")
  176. self.adc = ADCSettings(self._dev)
  177. self.glitch = GlitchSettings(self._dev)
  178. self.io = GPIOSettings(self._dev)
  179.  
  180. def arm(self, pin:int=0, edge:int=RISING_EDGE) -> None:
  181. """
  182. Arms the glitch/gpio/adc based on trigger pin
  183.  
  184. Arguments
  185. ---------
  186. pin : int
  187. Which pin to use for trigger [0:7]
  188. edge : int
  189. On what edge to trigger can be RISING_EDGE or FALLING_EDGE
  190.  
  191. Returns
  192. -------
  193. None
  194. """
  195. if pin < 0 or pin > 7:
  196. raise ValueError("Pin invalid")
  197. if edge != self.RISING_EDGE and edge != self.FALLING_EDGE:
  198. raise ValueError("Edge invalid")
  199.  
  200. self._dev.write(f":TRIGGER:PIN {pin},{edge}\n".encode("ascii"))
  201.  
  202. def trigger(self) -> None:
  203. """
  204. Immediately trigger the glitch/gpio/adc
  205.  
  206. Arguments
  207. ---------
  208. None
  209.  
  210. Returns
  211. -------
  212. None
  213. """
  214. self._dev.write(b":TRIGGER:NOW\n")
  215. def default_setup(self) -> None:
  216. """
  217. Load some safe defaults into settings
  218. """
  219. self.glitch.repeat = 10
  220. self.glitch.ext_offset = 0
  221. self.adc.delay = 0
  222. self.adc.clk_freq = 10000000
  223. self.io.reset()
  224.  
  225. def con(self) -> None:
  226. """
  227. Connect to device if serial port is not open
  228. """
  229. if not self._dev.is_open:
  230. self._dev.open()
  231.  
  232. def dis(self) -> None:
  233. """
  234. Disconnect from serial port
  235. """
  236. self._dev.close()
  237.  
  238. def get_last_trace(self, as_int:bool=False) -> Union[List[int], List[float]]:
  239. """
  240. Returns the latest captured data from ADC
  241.  
  242. Arguments
  243. ---------
  244. as_int : bool
  245. Returns the data as raw 10bit value from the adc
  246. Returns
  247. -------
  248. data : list<int>
  249. """
  250. self._dev.reset_input_buffer() #Clear any data
  251. self._dev.write(b":ADC:DATA?\n")
  252.  
  253. data = self._dev.readline()
  254. if data is None:
  255. return []
  256. data = data.decode("ascii").strip()
  257. if "ERR" in data:
  258. logging.warning(f"Received: {data}")
  259. return []
  260. data = data.split(",")
  261. data = [x for x in data if x != '']
  262. if as_int:
  263. return [int(x) for x in data]
  264. volt_per_step = 2 / 10 / 1024 # 2V pk-pk, 10x amplified from source, in 10-bit ADC
  265. return [(float(x)-512)*volt_per_step for x in data]
  266.  
  267. def plot_last_trace(self, continuous=False):
  268. try:
  269. import matplotlib.pyplot as plt
  270. except ImportError:
  271. print("Dependencies missing, please install python package matplotlib")
  272. return
  273. plt.ion()
  274. fig = plt.figure()
  275. ax = fig.add_subplot(111)
  276. ax.set_xlabel("Time since trigger (us)")
  277. ax.set_ylabel("Voltage difference (mV)")
  278. us_per_measurement = 1e6 / self.adc.clk_freq
  279. line, = ax.plot([float(x) * us_per_measurement for x in range(50000)], [0] * 50000, 'b-')
  280.  
  281. while True:
  282. try:
  283. res = self.get_last_trace()
  284. if len(res) != 50000:
  285. print(f"Got {len(res)} entries, skipping")
  286. if continuous:
  287. continue
  288. else:
  289. break
  290. trace = [x*1000 for x in res]
  291. line.set_ydata(trace)
  292. ax.relim()
  293. ax.autoscale_view()
  294. fig.canvas.draw()
  295. fig.canvas.flush_events()
  296. if continuous:
  297. self.trigger()
  298. continue
  299. else:
  300. plt.show()
  301. break
  302. except KeyboardInterrupt:
  303. break
  304.  
  305. def update(self):
  306. self._dev.write(b":BOOTLOADER\n")
  307.  
  308.  
  309. if __name__ == "__main__":
  310. s = Scope()
  311. s.default_setup()
  312. s.trigger()
  313. s.plot_last_trace(continuous=True)
Buy Me A Coffee