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:
74
scope.py
74
scope.py
@ -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
61
vm.py
@ -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 => Don’t 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:
|
||||
|
Reference in New Issue
Block a user