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

Simplifications; bug fixes; also, it turns out that the documentation is wrong about Macro trace modes supporting ClockTicks up to 64ki, it's actually 16ki

This commit is contained in:
2018-06-26 19:30:43 +01:00
parent e65792c79c
commit 7395fce34a
2 changed files with 95 additions and 64 deletions

View File

@ -16,10 +16,8 @@ import vm
LOG = logging.getLogger('scope')
class DotDict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
class ConfigurationError(Exception):
pass
class Scope(vm.VirtualMachine):
@ -48,20 +46,20 @@ class Scope(vm.VirtualMachine):
await self.issue_get_revision()
revision = ((await self.read_replies(2))[1]).decode('ascii')
if revision == 'BS000501':
self.awg_clock_period = 25e-9
self.capture_clock_period = 25e-9
self.capture_buffer_size = 12<<10
self.awg_clock_period = self.capture_clock_period
self.awg_wavetable_size = 1024
self.awg_sample_buffer_size = 1024
self.awg_minimum_clock = 33
self.awg_maximum_voltage = 3.3
self.analog_params = {'x1': self.AnalogParams(20, 300, 335, 355, 18.5, -7.585, 7.9, -5.5, 19e-3),
'x10': self.AnalogParams(20, 303, 350, 355, 187.3, -91.9, 64.2, -70.2, 234e-3)}
self.analog_params = {'x1': self.AnalogParams(20.2, 300, 339, 352, 18.5, -7.59, 8, -5.5, 19e-3),
'x10': self.AnalogParams(20.6, 302, 353, 351, 188, -92, 65.5, -70.9, 236e-3)}
self.analog_lo_min = 0.07
self.analog_hi_max = 0.88
self.logic_low = 0
self.logic_high = 3.3
self.capture_clock_period = 25e-9
self.capture_buffer_size = 12<<10
self.timeout_clock_period = 6.4e-6
self.timeout_clock_period = (1<<8) * self.capture_clock_period
self.timestamp_rollover = (1<<32) * self.capture_clock_period
else:
raise RuntimeError(f"Unsupported scope, revision: {revision}")
@ -120,30 +118,32 @@ class Scope(vm.VirtualMachine):
ticks = int(round(period / nsamples / self.capture_clock_period))
for capture_mode in vm.CaptureModes:
if capture_mode.analog_channels == len(analog_channels) and capture_mode.logic_channels == bool(logic_channels):
if ticks in range(capture_mode.clock_low, capture_mode.clock_high + 1):
clock_scale = 1
elif capture_mode.clock_divide and ticks > capture_mode.clock_high:
for clock_scale in range(2, 1<<16):
if ticks > capture_mode.clock_high and capture_mode.clock_divide:
for clock_scale in range(2, vm.Registers.ClockScale.maximum_value+1):
test_ticks = int(round(period / nsamples / self.capture_clock_period / clock_scale))
if test_ticks in range(capture_mode.clock_low, capture_mode.clock_high + 1):
ticks = test_ticks
break
else:
continue
break
elif ticks > capture_mode.clock_low:
clock_scale = 1
if ticks > capture_mode.clock_high:
ticks = capture_mode.clock_high
else:
continue
if capture_mode.clock_max is not None and ticks > capture_mode.clock_max:
ticks = capture_mode.clock_max
nsamples = int(round(period / ticks / self.capture_clock_period / clock_scale))
n = int(round(period / ticks / self.capture_clock_period / clock_scale))
if len(analog_channels) == 2:
nsamples -= nsamples % 2
n -= n % 2
buffer_width = self.capture_buffer_size // capture_mode.sample_width
if logic_channels and analog_channels:
buffer_width //= 2
if nsamples <= buffer_width:
if n <= buffer_width:
nsamples = n
break
else:
raise ValueError("Unable to find appropriate capture mode")
raise ConfigurationError("Unable to find appropriate capture mode")
analog_params = self.analog_params[probes]
if raw:
@ -207,8 +207,13 @@ class Scope(vm.VirtualMachine):
if timeout is None:
trigger_timeout = 0
else:
trigger_timeout = max(1, int(math.ceil(((trigger_intro+trigger_outro+trace_outro+2)*ticks*clock_scale*self.capture_clock_period
+ timeout)/self.timeout_clock_period)))
trigger_timeout = int(math.ceil(((trigger_intro+trigger_outro+trace_outro+2)*ticks*clock_scale*self.capture_clock_period
+ timeout)/self.timeout_clock_period))
if trigger_timeout > vm.Registers.Timeout.maximum_value:
if timeout > 0:
raise ConfigurationError("Required trigger timeout too long")
else:
raise ConfigurationError("Required trigger timeout too long, use a later trigger position")
sample_period = ticks*clock_scale*self.capture_clock_period
sample_rate = 1/sample_period
@ -242,7 +247,7 @@ class Scope(vm.VirtualMachine):
if capture_mode.analog_channels == 2:
address -= address % 2
traces = DotDict()
traces = vm.DotDict()
timestamps = array.array('d', (t*self.capture_clock_period for t in range(start_timestamp, timestamp, ticks*clock_scale)))
start_time = start_timestamp*self.capture_clock_period
for dump_channel, channel in enumerate(sorted(analog_channels)):
@ -255,7 +260,7 @@ class Scope(vm.VirtualMachine):
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))
data = await self.read_analog_samples(asamples, capture_mode.sample_width)
traces[channel] = DotDict({'timestamps': timestamps[dump_channel::len(analog_channels)] if len(analog_channels) > 1 else timestamps,
traces[channel] = vm.DotDict({'timestamps': timestamps[dump_channel::len(analog_channels)] if len(analog_channels) > 1 else timestamps,
'samples': array.array('d', (value*value_multiplier+value_offset for value in data)),
'start_time': start_time+sample_period*dump_channel,
'sample_period': sample_period*len(analog_channels),
@ -270,7 +275,7 @@ class Scope(vm.VirtualMachine):
data = await self.read_logic_samples(nsamples)
for i in logic_channels:
mask = 1<<i
traces[f'L{i}'] = DotDict({'timestamps': timestamps,
traces[f'L{i}'] = vm.DotDict({'timestamps': timestamps,
'samples': array.array('B', (1 if value & mask else 0 for value in data)),
'start_time': start_time,
'sample_period': sample_period,
@ -300,7 +305,7 @@ class Scope(vm.VirtualMachine):
if error < max_error:
possible_params.append((width if error == 0 else -error, (size, nwaves, clock, actualf)))
if not possible_params:
raise ValueError("No solution to required frequency/min_samples/max_error")
raise ConfigurationError("No solution to required frequency/min_samples/max_error")
size, nwaves, clock, actualf = sorted(possible_params)[-1][1]
async with self.transaction():
if wavetable is None:
@ -328,7 +333,7 @@ class Scope(vm.VirtualMachine):
await self.set_registers(KitchenSinkB=vm.KitchenSinkB.WaveformGeneratorEnable)
await self.issue_configure_device_hardware()
self._awg_running = True
Log.info(f"Signal generator running at {actualf:,0.1f}Hz")
LOG.info(f"Signal generator running at {actualf:0.1f}Hz")
return actualf
async def stop_generator(self):
@ -361,18 +366,22 @@ class Scope(vm.VirtualMachine):
async def calibrate(self, probes='x1', n=32):
import numpy as np
from scipy.optimize import least_squares
from scipy.optimize import least_squares, minimize
items = []
await self.start_generator(frequency=1000, waveform='square')
for lo in np.linspace(self.analog_lo_min, 0.5, n, endpoint=False):
for hi in np.linspace(self.analog_hi_max, 0.5, n):
if len(items) % 2 == 0:
traces = await self.capture(channels=['A','B'], period=2e-3, nsamples=2000, timeout=0, low=lo, high=hi, raw=True)
A = np.array(traces.A.samples)
B = np.array(traces.B.samples)
else:
A = np.array((await self.capture(channels=['A'], period=2e-3, nsamples=1000, timeout=0, low=lo, high=hi, raw=True)).A.samples)
B = np.array((await self.capture(channels=['B'], period=2e-3, nsamples=1000, timeout=0, low=lo, high=hi, raw=True)).B.samples)
A.sort()
Azero, Amax = A[25:475].mean(), A[525:975].mean()
if Azero < 0.01 or Amax > 0.99:
continue
B = np.array(traces.B.samples)
B.sort()
Bzero, Bmax = B[25:475].mean(), B[525:975].mean()
if Bzero < 0.01 or Bmax > 0.99:
@ -395,7 +404,11 @@ class Scope(vm.VirtualMachine):
lo, hi, low, high, offset = items
offset_mean = offset.mean()
LOG.info(f"Mean A-B offset: {offset_mean*1000:.1f}mV (+/- {100*offset.std()/offset_mean:.1f}%)")
params = self.analog_params[probes] = self.AnalogParams(*result.x, round(items[3,0],1), round(items[2,0],1), offset_mean)
def f(x):
lo, hi = self.calculate_lo_hi(x[0], x[1], result.x)
return np.sqrt((self.analog_lo_min - lo)**2 + (self.analog_hi_max - hi)**2)
safe_low, safe_high = minimize(f, (low[0],high[0])).x
params = self.analog_params[probes] = self.AnalogParams(*result.x, safe_high, safe_low, offset_mean)
LOG.info(f"Analog parameters: rd={params.rd:.1f}Ω rr={params.rr:.1f}Ω rt={params.rt:.1f}Ω rb={params.rb:.1f}"
f"scale={params.scale:.3f}V offset={params.offset:.3f}V "
f"safe_high={params.safe_high:.1f}V safe_low={params.safe_low:.1f}V")
@ -405,6 +418,7 @@ class Scope(vm.VirtualMachine):
LOG.info(f"Mean error: lo={lo_error*10000:.1f}bps hi={hi_error*10000:.1f}bps")
else:
LOG.warning(f"Calibration failed: {result.message}")
return items
return result.success

61
vm.py
View File

@ -24,6 +24,12 @@ import struct
LOG = logging.getLogger('vm')
class DotDict(dict):
__getattr__ = dict.__getitem__
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
def encode(self, value):
sign = self.dtype[0]
@ -58,14 +64,25 @@ class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
whole, fraction = map(int, self.dtype[1:].split('.', 1))
value = value / (1 << fraction)
return value
def calculate_width(self):
@property
def maximum_value(self):
if '.' in self.dtype:
whole, fraction = map(int, self.dtype[1:].split('.', 1))
else:
whole, fraction = int(self.dtype[1:]), 0
if self.dtype[0] == 'S':
whole -= 1
n = (1<<(whole+fraction)) - 1
return n / (1<<fraction) if fraction else n
@property
def width(self):
if '.' in self.dtype:
return sum(map(int, self.dtype[1:].split('.', 1))) // 8
else:
return int(self.dtype[1:]) // 8
Registers = {
Registers = DotDict({
"TriggerLogic": Register(0x05, 'U8', "Trigger Logic, one bit per channel (0 => Low, 1 => High)"),
"TriggerMask": Register(0x06, 'U8', "Trigger Mask, one bit per channel (0 => Dont Care, 1 => Active)"),
"SpockOption": Register(0x07, 'U8', "Spock Option Register (see bit definition table for details)"),
@ -138,7 +155,7 @@ Registers = {
"Map7": Register(0x9b, 'U8', "Peripheral Pin Select Channel 7"),
"MasterClockN": Register(0xf7, 'U8', "PLL prescale (DIV N)"),
"MasterClockM": Register(0xf8, 'U16', "PLL multiplier (MUL M)"),
}
})
class TraceMode(IntEnum):
Analog = 0
@ -199,27 +216,27 @@ class TraceStatus(IntEnum):
Wait = 0x02
Stop = 0x03
CaptureMode = namedtuple('CaptureMode', ('clock_low', 'clock_high', 'clock_max', 'analog_channels', 'sample_width',
CaptureMode = namedtuple('CaptureMode', ('clock_low', 'clock_high', 'analog_channels', 'sample_width',
'logic_channels', 'clock_divide', 'trace_mode', 'buffer_mode'))
CaptureModes = [
CaptureMode(40, 65535, None, 1, 2, False, False, TraceMode.Macro, BufferMode.Macro),
CaptureMode(40, 65535, None, 2, 2, False, False, TraceMode.MacroChop, BufferMode.MacroChop),
CaptureMode(15, 40, None, 1, 1, False, True, TraceMode.Analog, BufferMode.Single),
CaptureMode(13, 40, None, 2, 1, False, True, TraceMode.AnalogChop, BufferMode.Chop),
CaptureMode( 8, 14, None, 1, 1, False, False, TraceMode.AnalogFast, BufferMode.Single),
CaptureMode( 8, 40, None, 2, 1, False, False, TraceMode.AnalogFastChop, BufferMode.Chop),
CaptureMode( 2, 7, 5, 1, 1, False, False, TraceMode.AnalogShot, BufferMode.Single),
CaptureMode( 4, 7, 5, 2, 1, False, False, TraceMode.AnalogShotChop, BufferMode.Chop),
CaptureMode( 5, 16384, None, 0, 1, True, False, TraceMode.Logic, BufferMode.Single),
CaptureMode( 4, 4, None, 0, 1, True, False, TraceMode.LogicFast, BufferMode.Single),
CaptureMode( 1, 3, None, 0, 1, True, False, TraceMode.LogicShot, BufferMode.Single),
CaptureMode(15, 40, None, 1, 1, True, True, TraceMode.Mixed, BufferMode.Dual),
CaptureMode(13, 40, None, 2, 1, True, True, TraceMode.MixedChop, BufferMode.ChopDual),
CaptureMode( 8, 14, None, 1, 1, True, False, TraceMode.MixedFast, BufferMode.Dual),
CaptureMode( 8, 40, None, 2, 1, True, False, TraceMode.MixedFastChop, BufferMode.ChopDual),
CaptureMode( 2, 7, 5, 1, 1, True, False, TraceMode.MixedShot, BufferMode.Dual),
CaptureMode( 4, 7, 5, 2, 1, True, False, TraceMode.MixedShotChop, BufferMode.ChopDual),
CaptureMode(40, 16384, 1, 2, False, False, TraceMode.Macro, BufferMode.Macro),
CaptureMode(40, 16384, 2, 2, False, False, TraceMode.MacroChop, BufferMode.MacroChop),
CaptureMode(15, 40, 1, 1, False, True, TraceMode.Analog, BufferMode.Single),
CaptureMode(13, 40, 2, 1, False, True, TraceMode.AnalogChop, BufferMode.Chop),
CaptureMode( 8, 14, 1, 1, False, False, TraceMode.AnalogFast, BufferMode.Single),
CaptureMode( 8, 40, 2, 1, False, False, TraceMode.AnalogFastChop, BufferMode.Chop),
CaptureMode( 2, 5, 1, 1, False, False, TraceMode.AnalogShot, BufferMode.Single),
CaptureMode( 4, 5, 2, 1, False, False, TraceMode.AnalogShotChop, BufferMode.Chop),
CaptureMode( 5, 16384, 0, 1, True, False, TraceMode.Logic, BufferMode.Single),
CaptureMode( 4, 4, 0, 1, True, False, TraceMode.LogicFast, BufferMode.Single),
CaptureMode( 1, 3, 0, 1, True, False, TraceMode.LogicShot, BufferMode.Single),
CaptureMode(15, 40, 1, 1, True, True, TraceMode.Mixed, BufferMode.Dual),
CaptureMode(13, 40, 2, 1, True, True, TraceMode.MixedChop, BufferMode.ChopDual),
CaptureMode( 8, 14, 1, 1, True, False, TraceMode.MixedFast, BufferMode.Dual),
CaptureMode( 8, 40, 2, 1, True, False, TraceMode.MixedFastChop, BufferMode.ChopDual),
CaptureMode( 2, 5, 1, 1, True, False, TraceMode.MixedShot, BufferMode.Dual),
CaptureMode( 4, 5, 2, 1, True, False, TraceMode.MixedShotChop, BufferMode.ChopDual),
]
@ -328,7 +345,7 @@ class VirtualMachine:
register = Registers[name]
await self.issue(f'{register.base:02x}@p')
values = []
width = register.calculate_width()
width = register.width
for i in range(width):
values.append(int((await self.read_replies(2))[1], 16))
if i < width-1: