1
0
mirror of https://github.com/jonathanhogg/scopething synced 2025-07-14 11:12:09 +01:00

Some outstanding tweaks; fix to using arbitrary waveform; support for serial comms on Windows

This commit is contained in:
2018-09-05 15:49:22 +01:00
parent 00df1d7639
commit caacfe37fc
4 changed files with 91 additions and 48 deletions

View File

@ -13,6 +13,7 @@ import sys
from urllib.parse import urlparse from urllib.parse import urlparse
import streams import streams
from utils import DotDict
import vm import vm
@ -30,26 +31,29 @@ class ConfigurationError(Exception):
class Scope(vm.VirtualMachine): class Scope(vm.VirtualMachine):
AnalogParams = namedtuple('AnalogParams', ['la', 'lb', 'lc', 'ha', 'hb', 'hc', 'scale', 'offset', 'safe_low', 'safe_high', 'ab_offset']) class AnalogParams(namedtuple('AnalogParams', ['la', 'lb', 'lc', 'ha', 'hb', 'hc', 'scale', 'offset', 'safe_low', 'safe_high', 'ab_offset'])):
def __repr__(self):
return (f"la={self.la:.3f} lb={self.lb:.3e} lc={self.lc:.3e} ha={self.ha:.3f} hb={self.hb:.3e} hc={self.hc:.3e} "
f"scale={self.scale:.3f}V offset={self.offset:.3f}V safe_low={self.safe_low:.2f}V safe_high={self.safe_high:.2f}V "
f"ab_offset={self.ab_offset*1000:.1f}mV")
@classmethod async def connect(self, url):
async def connect(cls, url=None):
if url is None: if url is None:
port = next(streams.SerialStream.ports_matching(vid=0x0403, pid=0x6001)) port = next(streams.SerialStream.ports_matching(vid=0x0403, pid=0x6001))
url = f'file:{port.device}' url = f'file:{port.device}'
LOG.info(f"Connecting to scope at {url}") LOG.info(f"Connecting to scope at {url}")
self.close()
parts = urlparse(url, scheme='file') parts = urlparse(url, scheme='file')
if parts.scheme == 'file': if parts.scheme == 'file':
reader = writer = streams.SerialStream(device=parts.path) self._reader = self._writer = streams.SerialStream(device=parts.path)
elif parts.scheme == 'socket': elif parts.scheme == 'socket':
host, port = parts.netloc.split(':', 1) host, port = parts.netloc.split(':', 1)
reader, writer = await asyncio.open_connection(host, int(port)) self._reader, self._writer = await asyncio.open_connection(host, int(port))
else: else:
raise ValueError(f"Don't know what to do with url: {url}") raise ValueError(f"Don't know what to do with url: {url}")
scope = cls(reader, writer) self.url = url
scope.url = url await self.reset()
await scope.reset() return self
return scope
async def reset(self): async def reset(self):
LOG.info("Resetting scope") LOG.info("Resetting scope")
@ -65,7 +69,7 @@ class Scope(vm.VirtualMachine):
self.awg_minimum_clock = 33 self.awg_minimum_clock = 33
self.logic_low = 0 self.logic_low = 0
self.awg_maximum_voltage = self.clock_voltage = self.logic_high = 3.3 self.awg_maximum_voltage = self.clock_voltage = self.logic_high = 3.3
self.analog_params = {'x1': self.AnalogParams(1.1, -.05, 0, 1.1, -.05, -.05, 18.3, -7.50, -5.5, 8, 0)} self.analog_params = {'x1': self.AnalogParams(1.1, -.05, 0, 1.1, -.05, -.05, 18.333, -7.517, -5.5, 8, 0)}
self.analog_lo_min = 0.07 self.analog_lo_min = 0.07
self.analog_hi_max = 0.88 self.analog_hi_max = 0.88
self.timeout_clock_period = (1<<8) * self.master_clock_period self.timeout_clock_period = (1<<8) * self.master_clock_period
@ -84,7 +88,9 @@ class Scope(vm.VirtualMachine):
for url in config.sections(): for url in config.sections():
if url == self.url: if url == self.url:
for probes in config[url]: for probes in config[url]:
analog_params[probes] = self.AnalogParams(*map(float, config[url][probes].split())) params = self.AnalogParams(*map(float, config[url][probes].split()))
analog_params[probes] = params
LOG.debug(f"Loading saved parameters for {probes}: {params!r}")
if analog_params: if analog_params:
self.analog_params.update(analog_params) self.analog_params.update(analog_params)
LOG.info(f"Loaded analog parameters for probes: {', '.join(analog_params.keys())}") LOG.info(f"Loaded analog parameters for probes: {', '.join(analog_params.keys())}")
@ -112,7 +118,7 @@ class Scope(vm.VirtualMachine):
def calculate_lo_hi(self, low, high, params): def calculate_lo_hi(self, low, high, params):
if not isinstance(params, self.AnalogParams): if not isinstance(params, self.AnalogParams):
params = self.AnalogParams(*(list(params) + [None]*(11-len(params)))) params = self.AnalogParams(*list(params) + [None]*(11-len(params)))
l = (low - params.offset) / params.scale l = (low - params.offset) / params.scale
h = (high - params.offset) / params.scale h = (high - params.offset) / params.scale
dl = params.la*l + params.lb*h + params.lc dl = params.la*l + params.lb*h + params.lc
@ -234,19 +240,22 @@ class Scope(vm.VirtualMachine):
if channel.startswith('L'): if channel.startswith('L'):
channel = int(channel[1:]) channel = int(channel[1:])
else: else:
raise TypeError("Unrecognised trigger value") raise ValueError("Unrecognised trigger value")
if channel < 0 or channel > 7: if channel < 0 or channel > 7:
raise TypeError("Unrecognised trigger value") raise ValueError("Unrecognised trigger value")
mask = 1<<channel mask = 1<<channel
trigger_mask &= ~mask trigger_mask &= ~mask
if value: if value:
trigger_logic |= mask trigger_logic |= mask
else: else:
raise TypeError("Unrecognised trigger value") raise ValueError("Unrecognised trigger value")
if trigger_type.lower() in {'falling', 'below'}: trigger_type = trigger_type.lower()
if trigger_type in {'falling', 'below'}:
spock_option |= vm.SpockOption.TriggerInvert spock_option |= vm.SpockOption.TriggerInvert
elif trigger_type not in {'rising', 'above'}:
raise ValueError("Unrecognised trigger_type")
trigger_outro = 4 if hair_trigger else 8 trigger_outro = 4 if hair_trigger else 8
trigger_intro = 0 if trigger_type.lower() in {'above', 'below'} else trigger_outro trigger_intro = 0 if trigger_type in {'above', 'below'} else trigger_outro
trigger_samples = min(max(0, int(nsamples*trigger_position)), nsamples) trigger_samples = min(max(0, int(nsamples*trigger_position)), nsamples)
trace_outro = max(0, nsamples-trigger_samples-trigger_outro) trace_outro = max(0, nsamples-trigger_samples-trigger_outro)
trace_intro = max(0, trigger_samples-trigger_intro) trace_intro = max(0, trigger_samples-trigger_intro)
@ -292,7 +301,7 @@ class Scope(vm.VirtualMachine):
if capture_mode.analog_channels == 2: if capture_mode.analog_channels == 2:
address -= address % 2 address -= address % 2
traces = vm.DotDict() traces = DotDict()
timestamps = array.array('d', (t*self.master_clock_period for t in range(start_timestamp, timestamp, ticks*clock_scale))) timestamps = array.array('d', (t*self.master_clock_period for t in range(start_timestamp, timestamp, ticks*clock_scale)))
start_time = start_timestamp*self.master_clock_period start_time = start_timestamp*self.master_clock_period
for dump_channel, channel in enumerate(sorted(analog_channels)): for dump_channel, channel in enumerate(sorted(analog_channels)):
@ -305,9 +314,8 @@ class Scope(vm.VirtualMachine):
await self.issue_analog_dump_binary() await self.issue_analog_dump_binary()
value_multiplier, value_offset = (1, 0) if raw else (high-low, low-analog_params.ab_offset/2*(1 if channel == 'A' else -1)) value_multiplier, value_offset = (1, 0) if raw else (high-low, low-analog_params.ab_offset/2*(1 if channel == 'A' else -1))
data = await self.read_analog_samples(asamples, capture_mode.sample_width) data = await self.read_analog_samples(asamples, capture_mode.sample_width)
traces[channel] = vm.DotDict({'timestamps': timestamps[dump_channel::len(analog_channels)] if len(analog_channels) > 1 else timestamps, traces[channel] = DotDict({'timestamps': timestamps[dump_channel::len(analog_channels)] if len(analog_channels) > 1 else timestamps,
'samples': array.array('f', (value*value_multiplier+value_offset for value in data)), 'samples': array.array('f', (value*value_multiplier+value_offset for value in data)),
'start_time': start_time+sample_period*dump_channel,
'sample_period': sample_period*len(analog_channels), 'sample_period': sample_period*len(analog_channels),
'sample_rate': sample_rate/len(analog_channels), 'sample_rate': sample_rate/len(analog_channels),
'cause': cause}) 'cause': cause})
@ -320,9 +328,8 @@ class Scope(vm.VirtualMachine):
data = await self.read_logic_samples(nsamples) data = await self.read_logic_samples(nsamples)
for i in logic_channels: for i in logic_channels:
mask = 1<<i mask = 1<<i
traces[f'L{i}'] = vm.DotDict({'timestamps': timestamps, traces[f'L{i}'] = DotDict({'timestamps': timestamps,
'samples': array.array('B', (1 if value & mask else 0 for value in data)), 'samples': array.array('B', (1 if value & mask else 0 for value in data)),
'start_time': start_time,
'sample_period': sample_period, 'sample_period': sample_period,
'sample_rate': sample_rate, 'sample_rate': sample_rate,
'cause': cause}) 'cause': cause})
@ -362,10 +369,10 @@ class Scope(vm.VirtualMachine):
mode = {'sine': 0, 'triangle': 1, 'exponential': 2, 'square': 3}[waveform.lower()] mode = {'sine': 0, 'triangle': 1, 'exponential': 2, 'square': 3}[waveform.lower()]
await self.set_registers(Cmd=0, Mode=mode, Ratio=ratio) await self.set_registers(Cmd=0, Mode=mode, Ratio=ratio)
await self.issue_synthesize_wavetable() await self.issue_synthesize_wavetable()
elif len(wavetable) == self.awg_wavetable_size: elif len(waveform) == self.awg_wavetable_size:
wavetable = bytes(min(max(0, int(round(y*256))), 255) for y in wavetable) waveform = bytes(min(max(0, int(round(y*256))), 255) for y in waveform)
await self.set_registers(Cmd=0, Mode=1, Address=0, Size=1) await self.set_registers(Cmd=0, Mode=1, Address=0, Size=1)
await self.wavetable_write_bytes(wavetable) await self.wavetable_write_bytes(waveform)
else: else:
raise ValueError(f"waveform must be a valid name or a sequence of {self.awg_wavetable_size} samples [0,1)") raise ValueError(f"waveform must be a valid name or a sequence of {self.awg_wavetable_size} samples [0,1)")
async with self.transaction(): async with self.transaction():
@ -494,11 +501,7 @@ class Scope(vm.VirtualMachine):
safe_low, safe_high = minimize(f, (low[0], high[0])).x safe_low, safe_high = minimize(f, (low[0], high[0])).x
offset_mean = offset.mean() offset_mean = offset.mean()
params = self.analog_params[probes] = self.AnalogParams(*result.x, analog_scale, analog_offset, safe_low, safe_high, offset_mean) params = self.analog_params[probes] = self.AnalogParams(*result.x, analog_scale, analog_offset, safe_low, safe_high, offset_mean)
LOG.info(f"Analog parameters: la={params.la:.3e} lb={params.lb:.3e} lc={params.lc:.3e} " LOG.info(f"{params!r} ±{100*offset.std()/offset_mean:.1f}%)")
f"ha={params.ha:.3e} hb={params.hb:.3e} hc={params.hc:.3e} "
f"scale={params.scale:.3f}V offset={params.offset:.3f}V "
f"safe_low={params.safe_low:.1f}V safe_high={params.safe_high:.1f}V "
f"ab_offset={offset_mean*1000:.1f}mV (±{100*offset.std()/offset_mean:.1f}%)")
clo, chi = self.calculate_lo_hi(low, high, params) clo, chi = self.calculate_lo_hi(low, high, params)
lo_error = np.sqrt((((clo-lo)/(hi-lo))**2).mean()) lo_error = np.sqrt((((clo-lo)/(hi-lo))**2).mean())
hi_error = np.sqrt((((chi-hi)/(hi-lo))**2).mean()) hi_error = np.sqrt((((chi-hi)/(hi-lo))**2).mean())
@ -509,6 +512,9 @@ class Scope(vm.VirtualMachine):
LOG.warning(f"Calibration failed: {result.message}") LOG.warning(f"Calibration failed: {result.message}")
return result.success return result.success
def __repr__(self):
return f"<Scope {self.url}>"
""" """
$ ipython3 --pylab $ ipython3 --pylab
@ -536,7 +542,7 @@ async def main():
parser.add_argument('--verbose', action='store_true', default=False, help="Verbose logging") parser.add_argument('--verbose', action='store_true', default=False, help="Verbose logging")
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(level=logging.DEBUG if args.debug else (logging.INFO if args.verbose else logging.WARNING), stream=sys.stdout) logging.basicConfig(level=logging.DEBUG if args.debug else (logging.INFO if args.verbose else logging.WARNING), stream=sys.stdout)
s = await Scope.connect(args.url) s = await Scope().connect(args.url)
def await_(g): def await_(g):
task = asyncio.Task(g) task = asyncio.Task(g)

View File

@ -8,6 +8,7 @@ Package for asynchronous serial IO.
import asyncio import asyncio
import logging import logging
import sys import sys
import threading
import serial import serial
from serial.tools.list_ports import comports from serial.tools.list_ports import comports
@ -30,13 +31,16 @@ class SerialStream:
return SerialStream(port.device, **kwargs) return SerialStream(port.device, **kwargs)
raise RuntimeError("No matching serial device") raise RuntimeError("No matching serial device")
def __init__(self, device, loop=None, **kwargs): def __init__(self, device, use_threads=None, loop=None, **kwargs):
self._device = device self._device = device
self._connection = serial.Serial(self._device, timeout=0, write_timeout=0, **kwargs) self._use_threads = sys.platform == 'win32' if use_threads is None else use_threads
self._connection = serial.Serial(self._device, **kwargs) if self._use_threads else \
serial.Serial(self._device, timeout=0, write_timeout=0, **kwargs)
LOG.debug(f"Opened SerialStream on {device}") LOG.debug(f"Opened SerialStream on {device}")
self._loop = loop if loop is not None else asyncio.get_event_loop() self._loop = loop if loop is not None else asyncio.get_event_loop()
self._output_buffer = bytes() self._output_buffer = bytes()
self._output_buffer_empty = None self._output_buffer_empty = None
self._output_buffer_lock = threading.Lock() if self._use_threads else None
def __repr__(self): def __repr__(self):
return f'<{self.__class__.__name__}:{self._device}>' return f'<{self.__class__.__name__}:{self._device}>'
@ -47,6 +51,12 @@ class SerialStream:
self._connection = None self._connection = None
def write(self, data): def write(self, data):
if self._use_threads:
with self._output_buffer_lock:
self._output_buffer += data
if self._output_buffer_empty is None:
self._output_buffer_empty = self._loop.run_in_executor(None, self._write_blocking)
return
if not self._output_buffer: if not self._output_buffer:
try: try:
n = self._connection.write(data) n = self._connection.write(data)
@ -85,7 +95,22 @@ class SerialStream:
self._output_buffer_empty.set_result(None) self._output_buffer_empty.set_result(None)
self._output_buffer_empty = None self._output_buffer_empty = None
def _write_blocking(self):
with self._output_buffer_lock:
while self._output_buffer:
data = bytes(self._output_buffer)
self._output_buffer_lock.release()
try:
n = self._connection.write(data)
finally:
self._output_buffer_lock.acquire()
LOG.debug(f"Write {self._output_buffer[:n]!r}")
self._output_buffer = self._output_buffer[n:]
self._output_buffer_empty = None
async def read(self, n=None): async def read(self, n=None):
if self._use_threads:
return await self._loop.run_in_executor(None, self._read_blocking, n)
while True: while True:
w = self._connection.in_waiting w = self._connection.in_waiting
if w: if w:
@ -100,6 +125,14 @@ class SerialStream:
finally: finally:
self._loop.remove_reader(self._connection) self._loop.remove_reader(self._connection)
def _read_blocking(self, n=None):
data = self._connection.read(1)
w = self._connection.in_waiting
if w and (n is None or n > 1):
data += self._connection.read(w if n is None else min(n-1, w))
LOG.debug(f"Read {data!r}")
return data
async def readexactly(self, n): async def readexactly(self, n):
data = b'' data = b''
while len(data) < n: while len(data) < n:

8
utils.py Normal file
View File

@ -0,0 +1,8 @@
class DotDict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__

10
vm.py
View File

@ -20,16 +20,12 @@ from enum import IntEnum
import logging import logging
import struct import struct
from utils import DotDict
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DotDict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
class Register(namedtuple('Register', ['base', 'dtype', 'description'])): class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
def encode(self, value): def encode(self, value):
sign = self.dtype[0] sign = self.dtype[0]
@ -258,7 +254,7 @@ class VirtualMachine:
await self._vm.issue(self._data) await self._vm.issue(self._data)
return False return False
def __init__(self, reader, writer): def __init__(self, reader=None, writer=None):
self._reader = reader self._reader = reader
self._writer = writer self._writer = writer
self._transactions = [] self._transactions = []