mirror of
https://github.com/jonathanhogg/scopething
synced 2025-07-14 03:02:09 +01:00
Compare commits
3 Commits
751cacba6d
...
ca9011011d
Author | SHA1 | Date | |
---|---|---|---|
ca9011011d | |||
89e0f23499 | |||
c5222fd9b4 |
168
analysis.py
Normal file
168
analysis.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def interpolate_min_x(f, x):
|
||||||
|
return 0.5 * (f[x-1] - f[x+1]) / (f[x-1] - 2 * f[x] + f[x+1]) + x
|
||||||
|
|
||||||
|
|
||||||
|
def rms(f):
|
||||||
|
return np.sqrt((f ** 2).mean())
|
||||||
|
|
||||||
|
|
||||||
|
def sine_wave(n):
|
||||||
|
return np.sin(np.linspace(0, 2*np.pi, n, endpoint=False))
|
||||||
|
|
||||||
|
|
||||||
|
def triangle_wave(n):
|
||||||
|
x = np.linspace(0, 4, n, endpoint=False)
|
||||||
|
x2 = x % 2
|
||||||
|
y = np.where(x2 < 1, x2, 2 - x2)
|
||||||
|
y = np.where(x // 2 < 1, y, -y)
|
||||||
|
return y
|
||||||
|
|
||||||
|
|
||||||
|
def square_wave(n, duty=0.5):
|
||||||
|
w = int(n * duty)
|
||||||
|
return np.hstack([np.ones(w), -np.ones(n - w)])
|
||||||
|
|
||||||
|
|
||||||
|
def sawtooth_wave(n):
|
||||||
|
return 2 * (np.linspace(0.5, 1.5, n, endpoint=False) % 1) - 1
|
||||||
|
|
||||||
|
|
||||||
|
def moving_average(samples, width, mode='wrap'):
|
||||||
|
hwidth = width // 2
|
||||||
|
samples = np.take(samples, np.arange(-hwidth, len(samples)+width-hwidth), mode=mode)
|
||||||
|
cumulative = samples.cumsum()
|
||||||
|
return (cumulative[width:] - cumulative[:-width]) / width
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_periodicity(series, window=0.1):
|
||||||
|
samples = np.array(series.samples)
|
||||||
|
window = int(len(samples) * window)
|
||||||
|
errors = np.zeros(len(samples) - window)
|
||||||
|
for i in range(1, len(errors) + 1):
|
||||||
|
errors[i-1] = rms(samples[i:] - samples[:-i])
|
||||||
|
threshold = errors.max() / 2
|
||||||
|
minima = []
|
||||||
|
for i in range(window, len(errors) - window):
|
||||||
|
p = errors[i-window:i+window].argmin()
|
||||||
|
if p == window and errors[p + i - window] < threshold:
|
||||||
|
minima.append(interpolate_min_x(errors, i))
|
||||||
|
if len(minima) <= 1:
|
||||||
|
return None
|
||||||
|
ks = np.polyfit(np.arange(0, len(minima)), minima, 1)
|
||||||
|
return ks[0] / series.sample_rate
|
||||||
|
|
||||||
|
|
||||||
|
def extract_waveform(series, period):
|
||||||
|
p = int(round(series.sample_rate * period))
|
||||||
|
n = len(series.samples) // p
|
||||||
|
if n <= 2:
|
||||||
|
return None, None
|
||||||
|
samples = np.array(series.samples)[:p*n]
|
||||||
|
cumsum = samples.cumsum()
|
||||||
|
underlying = (cumsum[p:] - cumsum[:-p]) / p
|
||||||
|
n -= 1
|
||||||
|
samples = samples[p//2:p*n + p//2] - underlying
|
||||||
|
wave = np.zeros(p)
|
||||||
|
for i in range(n):
|
||||||
|
o = i * p
|
||||||
|
wave += samples[o:o+p]
|
||||||
|
wave /= n
|
||||||
|
return wave, p//2, n, underlying
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_waveform(samples, smooth=7):
|
||||||
|
n = len(samples)
|
||||||
|
smoothed = moving_average(samples, smooth)
|
||||||
|
scale = (smoothed.max() - smoothed.min()) / 2
|
||||||
|
offset = (smoothed.max() + smoothed.min()) / 2
|
||||||
|
smoothed -= offset
|
||||||
|
last_rising = first_falling = None
|
||||||
|
crossings = []
|
||||||
|
for i in range(n):
|
||||||
|
if smoothed[i-1] < 0 and smoothed[i] > 0:
|
||||||
|
last_rising = i
|
||||||
|
elif smoothed[i-1] > 0 and smoothed[i] < 0:
|
||||||
|
if last_rising is None:
|
||||||
|
first_falling = i
|
||||||
|
else:
|
||||||
|
crossings.append((i - last_rising, last_rising))
|
||||||
|
if first_falling is not None:
|
||||||
|
crossings.append((n + first_falling - last_rising, last_rising))
|
||||||
|
width, first = min(crossings)
|
||||||
|
wave = np.hstack([smoothed[first:], smoothed[:first]]) / scale
|
||||||
|
return wave, offset, scale, first, sorted((i - first % n, w) for (w, i) in crossings)
|
||||||
|
|
||||||
|
|
||||||
|
def characterize_waveform(samples, crossings):
|
||||||
|
n = len(samples)
|
||||||
|
possibles = []
|
||||||
|
if len(crossings) == 1:
|
||||||
|
duty_cycle = crossings[0][1] / n
|
||||||
|
if duty_cycle > 0.45 and duty_cycle < 0.55:
|
||||||
|
possibles.append((rms(samples - sine_wave(n)), 'sine', None))
|
||||||
|
possibles.append((rms(samples - triangle_wave(n)), 'triangle', None))
|
||||||
|
possibles.append((rms(samples - sawtooth_wave(n)), 'sawtooth', None))
|
||||||
|
possibles.append((rms(samples - square_wave(n, duty_cycle)), 'square', duty_cycle))
|
||||||
|
possibles.sort()
|
||||||
|
return possibles
|
||||||
|
|
||||||
|
|
||||||
|
def analyze_series(series):
|
||||||
|
period = calculate_periodicity(series)
|
||||||
|
if period is not None:
|
||||||
|
waveform = DotDict(period=period, frequency=1 / period)
|
||||||
|
wave, start, count, underlying = extract_waveform(series, period)
|
||||||
|
wave, offset, scale, first, crossings = normalize_waveform(wave)
|
||||||
|
waveform.samples = wave
|
||||||
|
waveform.beginning = start + first
|
||||||
|
waveform.count = count
|
||||||
|
waveform.amplitude = scale
|
||||||
|
waveform.offset = underlying.mean() + offset
|
||||||
|
possibles = characterize_waveform(wave, crossings)
|
||||||
|
if possibles:
|
||||||
|
error, shape, duty_cycle = possibles[0]
|
||||||
|
waveform.error = error
|
||||||
|
waveform.shape = shape
|
||||||
|
if duty_cycle is not None:
|
||||||
|
waveform.duty_cycle = duty_cycle
|
||||||
|
else:
|
||||||
|
waveform.shape = 'unknown'
|
||||||
|
series.waveform = waveform
|
||||||
|
|
||||||
|
|
||||||
|
# %%
|
||||||
|
|
||||||
|
from pylab import figure, plot, show
|
||||||
|
from utils import DotDict
|
||||||
|
|
||||||
|
o = 400
|
||||||
|
m = 5
|
||||||
|
n = o * m
|
||||||
|
samples = square_wave(o)
|
||||||
|
samples = np.hstack([samples] * m) * 2
|
||||||
|
samples = np.hstack([samples[100:], samples[:100]])
|
||||||
|
samples += np.random.normal(size=n) * 0.1
|
||||||
|
samples += np.linspace(4.5, 5.5, n)
|
||||||
|
series = DotDict(samples=samples, sample_rate=1000000)
|
||||||
|
|
||||||
|
analyze_series(series)
|
||||||
|
|
||||||
|
if 'waveform' in series:
|
||||||
|
waveform = series.waveform
|
||||||
|
if 'duty_cycle' in waveform:
|
||||||
|
print(f"Found {waveform.frequency:.0f}Hz {waveform.shape} wave, "
|
||||||
|
f"with duty cycle {waveform.duty_cycle * 100:.0f}%, "
|
||||||
|
f"amplitude ±{waveform.amplitude:.1f}V and offset {waveform.offset:.1f}V")
|
||||||
|
else:
|
||||||
|
print(f"Found {waveform.frequency:.0f}Hz {waveform.shape} wave, "
|
||||||
|
f"with amplitude ±{waveform.amplitude:.1f}V and offset {waveform.offset:.1f}V")
|
||||||
|
|
||||||
|
figure(1)
|
||||||
|
plot(series.samples)
|
||||||
|
wave = np.hstack([waveform.samples[-waveform.beginning:]] + [waveform.samples] * waveform.count + [waveform.samples[:-waveform.beginning]])
|
||||||
|
plot(wave * waveform.amplitude + waveform.offset)
|
||||||
|
show()
|
97
scope.py
97
scope.py
@ -16,9 +16,8 @@ from utils import DotDict
|
|||||||
import vm
|
import vm
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
Log = logging.getLogger(__name__)
|
||||||
|
AnalogParametersPath = Path('~/.config/scopething/analog.conf').expanduser()
|
||||||
ANALOG_PARAMETERS_PATH = Path('~/.config/scopething/analog.conf').expanduser()
|
|
||||||
|
|
||||||
|
|
||||||
class UsageError(Exception):
|
class UsageError(Exception):
|
||||||
@ -44,7 +43,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("No matching serial device found")
|
raise RuntimeError("No matching serial device found")
|
||||||
LOG.info(f"Connecting to scope at {url}")
|
Log.info(f"Connecting to scope at {url}")
|
||||||
self.close()
|
self.close()
|
||||||
parts = urlparse(url, scheme='file')
|
parts = urlparse(url, scheme='file')
|
||||||
if parts.scheme == 'file':
|
if parts.scheme == 'file':
|
||||||
@ -59,7 +58,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
async def reset(self):
|
async def reset(self):
|
||||||
LOG.info("Resetting scope")
|
Log.info("Resetting scope")
|
||||||
await self.issue_reset()
|
await self.issue_reset()
|
||||||
await self.issue_get_revision()
|
await self.issue_get_revision()
|
||||||
revision = ((await self.read_replies(2))[1]).decode('ascii')
|
revision = ((await self.read_replies(2))[1]).decode('ascii')
|
||||||
@ -82,31 +81,31 @@ class Scope(vm.VirtualMachine):
|
|||||||
self._awg_running = False
|
self._awg_running = False
|
||||||
self._clock_running = False
|
self._clock_running = False
|
||||||
self.load_analog_params()
|
self.load_analog_params()
|
||||||
LOG.info(f"Initialised scope, revision: {revision}")
|
Log.info(f"Initialised scope, revision: {revision}")
|
||||||
|
|
||||||
def load_analog_params(self):
|
def load_analog_params(self):
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(ANALOG_PARAMETERS_PATH)
|
config.read(AnalogParametersPath)
|
||||||
analog_params = {}
|
analog_params = {}
|
||||||
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]:
|
||||||
params = self.AnalogParams(*map(float, config[url][probes].split()))
|
params = self.AnalogParams(*map(float, config[url][probes].split()))
|
||||||
analog_params[probes] = params
|
analog_params[probes] = params
|
||||||
LOG.debug(f"Loading saved parameters for {probes}: {params!r}")
|
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())}")
|
||||||
|
|
||||||
def save_analog_params(self):
|
def save_analog_params(self):
|
||||||
LOG.info("Saving analog parameters")
|
Log.info("Saving analog parameters")
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read(ANALOG_PARAMETERS_PATH)
|
config.read(AnalogParametersPath)
|
||||||
config[self.url] = {probes: ' '.join(map(str, self.analog_params[probes])) for probes in self.analog_params}
|
config[self.url] = {probes: ' '.join(map(str, self.analog_params[probes])) for probes in self.analog_params}
|
||||||
parent = ANALOG_PARAMETERS_PATH.parent
|
parent = AnalogParametersPath.parent
|
||||||
if not parent.is_dir():
|
if not parent.is_dir():
|
||||||
parent.mkdir(parents=True)
|
parent.mkdir(parents=True)
|
||||||
with open(ANALOG_PARAMETERS_PATH, 'w') as parameters_file:
|
with open(AnalogParametersPath, 'w') as parameters_file:
|
||||||
config.write(parameters_file)
|
config.write(parameters_file)
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
@ -117,7 +116,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
super().close()
|
super().close()
|
||||||
LOG.info("Closed scope")
|
Log.info("Closed scope")
|
||||||
|
|
||||||
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):
|
||||||
@ -164,20 +163,20 @@ class Scope(vm.VirtualMachine):
|
|||||||
ticks = int(round(period / self.master_clock_period / nsamples))
|
ticks = int(round(period / self.master_clock_period / nsamples))
|
||||||
clock_scale = 1
|
clock_scale = 1
|
||||||
if capture_mode.analog_channels == len(analog_channels) and capture_mode.logic_channels == bool(logic_channels):
|
if capture_mode.analog_channels == len(analog_channels) and capture_mode.logic_channels == bool(logic_channels):
|
||||||
LOG.debug(f"Considering trace mode {capture_mode.trace_mode.name}...")
|
Log.debug(f"Considering trace mode {capture_mode.trace_mode.name}...")
|
||||||
if ticks > capture_mode.clock_high and capture_mode.clock_divide > 1:
|
if ticks > capture_mode.clock_high and capture_mode.clock_divide > 1:
|
||||||
clock_scale = int(math.ceil(period / self.master_clock_period / nsamples / capture_mode.clock_high))
|
clock_scale = int(math.ceil(period / self.master_clock_period / nsamples / capture_mode.clock_high))
|
||||||
ticks = int(round(period / self.master_clock_period / nsamples / clock_scale))
|
ticks = int(round(period / self.master_clock_period / nsamples / clock_scale))
|
||||||
if ticks in range(capture_mode.clock_low, capture_mode.clock_high+1):
|
if ticks in range(capture_mode.clock_low, capture_mode.clock_high+1):
|
||||||
LOG.debug(f"- try with tick count {ticks} x {clock_scale}")
|
Log.debug(f"- try with tick count {ticks} x {clock_scale}")
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
elif ticks >= capture_mode.clock_low:
|
elif ticks >= capture_mode.clock_low:
|
||||||
if ticks > capture_mode.clock_high:
|
if ticks > capture_mode.clock_high:
|
||||||
ticks = capture_mode.clock_high
|
ticks = capture_mode.clock_high
|
||||||
LOG.debug(f"- try with tick count {ticks}")
|
Log.debug(f"- try with tick count {ticks}")
|
||||||
else:
|
else:
|
||||||
LOG.debug("- mode too slow")
|
Log.debug("- mode too slow")
|
||||||
continue
|
continue
|
||||||
n = int(round(period / self.master_clock_period / ticks / clock_scale))
|
n = int(round(period / self.master_clock_period / ticks / clock_scale))
|
||||||
if len(analog_channels) == 2:
|
if len(analog_channels) == 2:
|
||||||
@ -186,16 +185,16 @@ class Scope(vm.VirtualMachine):
|
|||||||
if logic_channels and analog_channels:
|
if logic_channels and analog_channels:
|
||||||
buffer_width //= 2
|
buffer_width //= 2
|
||||||
if n <= buffer_width:
|
if n <= buffer_width:
|
||||||
LOG.debug(f"- OK; period is {n} samples")
|
Log.debug(f"- OK; period is {n} samples")
|
||||||
nsamples = n
|
nsamples = n
|
||||||
break
|
break
|
||||||
LOG.debug(f"- insufficient buffer space for necessary {n} samples")
|
Log.debug(f"- insufficient buffer space for necessary {n} samples")
|
||||||
else:
|
else:
|
||||||
raise ConfigurationError("Unable to find appropriate capture mode")
|
raise ConfigurationError("Unable to find appropriate capture mode")
|
||||||
sample_period = ticks*clock_scale*self.master_clock_period
|
sample_period = ticks*clock_scale*self.master_clock_period
|
||||||
sample_rate = 1/sample_period
|
sample_rate = 1/sample_period
|
||||||
if trigger_position and sample_rate > 5e6:
|
if trigger_position and sample_rate > 5e6:
|
||||||
LOG.warn("Pre-trigger capture not supported above 5M samples/s; forcing trigger_position=0")
|
Log.warn("Pre-trigger capture not supported above 5M samples/s; forcing trigger_position=0")
|
||||||
trigger_position = 0
|
trigger_position = 0
|
||||||
|
|
||||||
if raw:
|
if raw:
|
||||||
@ -206,11 +205,11 @@ class Scope(vm.VirtualMachine):
|
|||||||
if low is None:
|
if low is None:
|
||||||
low = analog_params.safe_low if analog_channels else self.logic_low
|
low = analog_params.safe_low if analog_channels else self.logic_low
|
||||||
elif low < analog_params.safe_low:
|
elif low < analog_params.safe_low:
|
||||||
LOG.warning(f"Voltage range is below safe minimum: {low} < {analog_params.safe_low}")
|
Log.warning(f"Voltage range is below safe minimum: {low} < {analog_params.safe_low}")
|
||||||
if high is None:
|
if high is None:
|
||||||
high = analog_params.safe_high if analog_channels else self.logic_high
|
high = analog_params.safe_high if analog_channels else self.logic_high
|
||||||
elif high > analog_params.safe_high:
|
elif high > analog_params.safe_high:
|
||||||
LOG.warning(f"Voltage range is above safe maximum: {high} > {analog_params.safe_high}")
|
Log.warning(f"Voltage range is above safe maximum: {high} > {analog_params.safe_high}")
|
||||||
lo, hi = self.calculate_lo_hi(low, high, analog_params)
|
lo, hi = self.calculate_lo_hi(low, high, analog_params)
|
||||||
|
|
||||||
spock_option = vm.SpockOption.TriggerTypeHardwareComparator
|
spock_option = vm.SpockOption.TriggerTypeHardwareComparator
|
||||||
@ -225,8 +224,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
kitchen_sink_b |= vm.KitchenSinkB.AnalogFilterEnable
|
kitchen_sink_b |= vm.KitchenSinkB.AnalogFilterEnable
|
||||||
if trigger_level is None:
|
if trigger_level is None:
|
||||||
trigger_level = (high + low) / 2
|
trigger_level = (high + low) / 2
|
||||||
if not raw:
|
analog_trigger_level = (trigger_level - analog_params.offset) / analog_params.scale if not raw else trigger_level
|
||||||
trigger_level = (trigger_level - analog_params.offset) / analog_params.scale
|
|
||||||
if trigger == 'A' or trigger == 'B':
|
if trigger == 'A' or trigger == 'B':
|
||||||
if trigger == 'A':
|
if trigger == 'A':
|
||||||
spock_option |= vm.SpockOption.TriggerSourceA
|
spock_option |= vm.SpockOption.TriggerSourceA
|
||||||
@ -273,12 +271,12 @@ class Scope(vm.VirtualMachine):
|
|||||||
else:
|
else:
|
||||||
raise ConfigurationError("Required trigger timeout too long, use a later trigger position")
|
raise ConfigurationError("Required trigger timeout too long, use a later trigger position")
|
||||||
|
|
||||||
LOG.info(f"Begin {('mixed' if logic_channels else 'analogue') if analog_channels else 'logic'} signal capture "
|
Log.info(f"Begin {('mixed' if logic_channels else 'analogue') if analog_channels else 'logic'} signal capture "
|
||||||
f"at {sample_rate:,.0f} samples per second (trace mode {capture_mode.trace_mode.name})")
|
f"at {sample_rate:,.0f} samples per second (trace mode {capture_mode.trace_mode.name})")
|
||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
await self.set_registers(TraceMode=capture_mode.trace_mode, BufferMode=capture_mode.buffer_mode,
|
await self.set_registers(TraceMode=capture_mode.trace_mode, BufferMode=capture_mode.buffer_mode,
|
||||||
SampleAddress=0, ClockTicks=ticks, ClockScale=clock_scale,
|
SampleAddress=0, ClockTicks=ticks, ClockScale=clock_scale,
|
||||||
TriggerLevel=trigger_level, TriggerLogic=trigger_logic, TriggerMask=trigger_mask,
|
TriggerLevel=analog_trigger_level, TriggerLogic=trigger_logic, TriggerMask=trigger_mask,
|
||||||
TraceIntro=trace_intro, TraceOutro=trace_outro, TraceDelay=0, Timeout=trigger_timeout,
|
TraceIntro=trace_intro, TraceOutro=trace_outro, TraceDelay=0, Timeout=trigger_timeout,
|
||||||
TriggerIntro=trigger_intro//2, TriggerOutro=trigger_outro//2, Prelude=0,
|
TriggerIntro=trigger_intro//2, TriggerOutro=trigger_outro//2, Prelude=0,
|
||||||
SpockOption=spock_option, ConverterLo=lo, ConverterHi=hi,
|
SpockOption=spock_option, ConverterLo=lo, ConverterHi=hi,
|
||||||
@ -304,7 +302,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
address -= address % 2
|
address -= address % 2
|
||||||
|
|
||||||
traces = 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(0, timestamp, ticks*clock_scale)))
|
||||||
for dump_channel, channel in enumerate(sorted(analog_channels)):
|
for dump_channel, channel in enumerate(sorted(analog_channels)):
|
||||||
asamples = nsamples // len(analog_channels)
|
asamples = nsamples // len(analog_channels)
|
||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
@ -315,11 +313,18 @@ 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] = DotDict({'timestamps': timestamps[dump_channel::len(analog_channels)] if len(analog_channels) > 1 else timestamps,
|
series = DotDict({'channel': channel,
|
||||||
|
'start_timestamp': start_timestamp,
|
||||||
|
'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)),
|
||||||
'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})
|
||||||
|
if cause == 'trigger' and channel == trigger:
|
||||||
|
series.trigger_timestamp = series.timestamps[trigger_samples // len(analog_channels)]
|
||||||
|
series.trigger_level = trigger_level
|
||||||
|
series.trigger_type = trigger_type
|
||||||
|
traces[channel] = series
|
||||||
if logic_channels:
|
if logic_channels:
|
||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
await self.set_registers(SampleAddress=(address - nsamples) % buffer_width,
|
await self.set_registers(SampleAddress=(address - nsamples) % buffer_width,
|
||||||
@ -329,12 +334,20 @@ 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}'] = DotDict({'timestamps': timestamps,
|
channel = f'L{i}'
|
||||||
|
series = DotDict({'channel': channel,
|
||||||
|
'start_timestamp': start_timestamp,
|
||||||
|
'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)),
|
||||||
'sample_period': sample_period,
|
'sample_period': sample_period,
|
||||||
'sample_rate': sample_rate,
|
'sample_rate': sample_rate,
|
||||||
'cause': cause})
|
'cause': cause})
|
||||||
LOG.info(f"{nsamples} samples captured on {cause}, traces: {', '.join(traces)}")
|
if cause == 'trigger' and isinstance(trigger, dict) and i in trigger:
|
||||||
|
series.trigger_timestamp = series.timestamps[trigger_samples]
|
||||||
|
series.trigger_level = trigger[i]
|
||||||
|
series.trigger_type = trigger_type
|
||||||
|
traces[channel] = series
|
||||||
|
Log.info(f"{nsamples} samples captured on {cause}, traces: {', '.join(traces)}")
|
||||||
return traces
|
return traces
|
||||||
|
|
||||||
async def start_waveform(self, frequency, waveform='sine', ratio=0.5, low=0, high=None, min_samples=50, max_error=1e-4):
|
async def start_waveform(self, frequency, waveform='sine', ratio=0.5, low=0, high=None, min_samples=50, max_error=1e-4):
|
||||||
@ -355,7 +368,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
size = int(round(nwaves * width))
|
size = int(round(nwaves * width))
|
||||||
actualf = self.master_clock_rate * nwaves / size / clock
|
actualf = self.master_clock_rate * nwaves / size / clock
|
||||||
if actualf == frequency:
|
if actualf == frequency:
|
||||||
LOG.debug(f"Exact solution: size={size} nwaves={nwaves} clock={clock}")
|
Log.debug(f"Exact solution: size={size} nwaves={nwaves} clock={clock}")
|
||||||
break
|
break
|
||||||
error = abs(frequency - actualf) / frequency
|
error = abs(frequency - actualf) / frequency
|
||||||
if error < max_error and (best_solution is None or error < best_solution[0]):
|
if error < max_error and (best_solution is None or error < best_solution[0]):
|
||||||
@ -364,7 +377,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
if best_solution is None:
|
if best_solution is None:
|
||||||
raise ConfigurationError("No solution to required frequency/min_samples/max_error")
|
raise ConfigurationError("No solution to required frequency/min_samples/max_error")
|
||||||
error, size, nwaves, clock, actualf = best_solution
|
error, size, nwaves, clock, actualf = best_solution
|
||||||
LOG.debug(f"Best solution: size={size} nwaves={nwaves} clock={clock} actualf={actualf}")
|
Log.debug(f"Best solution: size={size} nwaves={nwaves} clock={clock} actualf={actualf}")
|
||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
if isinstance(waveform, str):
|
if isinstance(waveform, str):
|
||||||
mode = {'sine': 0, 'triangle': 1, 'exponential': 2, 'square': 3}[waveform.lower()]
|
mode = {'sine': 0, 'triangle': 1, 'exponential': 2, 'square': 3}[waveform.lower()]
|
||||||
@ -391,7 +404,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
await self.set_registers(KitchenSinkB=vm.KitchenSinkB.WaveformGeneratorEnable)
|
await self.set_registers(KitchenSinkB=vm.KitchenSinkB.WaveformGeneratorEnable)
|
||||||
await self.issue_configure_device_hardware()
|
await self.issue_configure_device_hardware()
|
||||||
self._awg_running = True
|
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
|
return actualf
|
||||||
|
|
||||||
async def stop_waveform(self):
|
async def stop_waveform(self):
|
||||||
@ -402,7 +415,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
await self.issue_control_clock_generator()
|
await self.issue_control_clock_generator()
|
||||||
await self.set_registers(KitchenSinkB=0)
|
await self.set_registers(KitchenSinkB=0)
|
||||||
await self.issue_configure_device_hardware()
|
await self.issue_configure_device_hardware()
|
||||||
LOG.info("Signal generator stopped")
|
Log.info("Signal generator stopped")
|
||||||
self._awg_running = False
|
self._awg_running = False
|
||||||
|
|
||||||
async def start_clock(self, frequency, ratio=0.5, max_error=1e-4):
|
async def start_clock(self, frequency, ratio=0.5, max_error=1e-4):
|
||||||
@ -417,7 +430,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
await self.set_registers(Map5=0x12, Clock=ticks, Rise=0, Fall=fall, Control=0x80, Cmd=3, Mode=0)
|
await self.set_registers(Map5=0x12, Clock=ticks, Rise=0, Fall=fall, Control=0x80, Cmd=3, Mode=0)
|
||||||
await self.issue_control_clock_generator()
|
await self.issue_control_clock_generator()
|
||||||
self._clock_running = True
|
self._clock_running = True
|
||||||
LOG.info(f"Clock generator running at {actualf:0.1f}Hz, {actualr*100:.0f}% duty cycle")
|
Log.info(f"Clock generator running at {actualf:0.1f}Hz, {actualr*100:.0f}% duty cycle")
|
||||||
return actualf, actualr
|
return actualf, actualr
|
||||||
|
|
||||||
async def stop_clock(self):
|
async def stop_clock(self):
|
||||||
@ -426,7 +439,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
async with self.transaction():
|
async with self.transaction():
|
||||||
await self.set_registers(Map5=0, Cmd=1, Mode=0)
|
await self.set_registers(Map5=0, Cmd=1, Mode=0)
|
||||||
await self.issue_control_clock_generator()
|
await self.issue_control_clock_generator()
|
||||||
LOG.info("Clock generator stopped")
|
Log.info("Clock generator stopped")
|
||||||
self._clock_running = False
|
self._clock_running = False
|
||||||
|
|
||||||
async def calibrate(self, probes='x1', n=32, save=True):
|
async def calibrate(self, probes='x1', n=32, save=True):
|
||||||
@ -477,7 +490,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
full = (full + 1) / 3
|
full = (full + 1) / 3
|
||||||
analog_scale = self.clock_voltage / (full - zero)
|
analog_scale = self.clock_voltage / (full - zero)
|
||||||
analog_offset = -zero * analog_scale
|
analog_offset = -zero * analog_scale
|
||||||
LOG.info(f"Analog full range = {analog_scale:.2f}V, zero offset = {analog_offset:.2f}V")
|
Log.info(f"Analog full range = {analog_scale:.2f}V, zero offset = {analog_offset:.2f}V")
|
||||||
for lo in np.linspace(self.analog_lo_min, 0.5, n, endpoint=False):
|
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):
|
for hi in np.linspace(self.analog_hi_max, 0.5, n):
|
||||||
zero, full, offset = await measure(lo, hi, 2e-3 if len(items) % 4 < 2 else 1e-3, len(items) % 2 == 0)
|
zero, full, offset = await measure(lo, hi, 2e-3 if len(items) % 4 < 2 else 1e-3, len(items) % 2 == 0)
|
||||||
@ -497,7 +510,7 @@ class Scope(vm.VirtualMachine):
|
|||||||
constraints=[{'type': 'eq', 'fun': lambda x: x[0]*1/3 + x[1]*2/3 + x[2] - 1/3},
|
constraints=[{'type': 'eq', 'fun': lambda x: x[0]*1/3 + x[1]*2/3 + x[2] - 1/3},
|
||||||
{'type': 'eq', 'fun': lambda x: x[3]*2/3 + x[4]*1/3 + x[5] - 2/3}])
|
{'type': 'eq', 'fun': lambda x: x[3]*2/3 + x[4]*1/3 + x[5] - 2/3}])
|
||||||
if result.success:
|
if result.success:
|
||||||
LOG.info(f"Calibration succeeded: {result.message}")
|
Log.info(f"Calibration succeeded: {result.message}")
|
||||||
params = self.AnalogParams(*result.x, analog_scale, analog_offset, None, None, None)
|
params = self.AnalogParams(*result.x, analog_scale, analog_offset, None, None, None)
|
||||||
|
|
||||||
def f(x):
|
def f(x):
|
||||||
@ -507,15 +520,15 @@ 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"{params!r} ±{100*offset.std()/offset_mean:.1f}%)")
|
Log.info(f"{params!r} ±{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())
|
||||||
LOG.info(f"Mean error: lo={lo_error*10000:.1f}bps hi={hi_error*10000:.1f}bps")
|
Log.info(f"Mean error: lo={lo_error*10000:.1f}bps hi={hi_error*10000:.1f}bps")
|
||||||
if save:
|
if save:
|
||||||
self.save_analog_params()
|
self.save_analog_params()
|
||||||
else:
|
else:
|
||||||
LOG.warning(f"Calibration failed: {result.message}")
|
Log.warning(f"Calibration failed: {result.message}")
|
||||||
return result.success
|
return result.success
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
|
18
streams.py
18
streams.py
@ -14,7 +14,7 @@ import serial
|
|||||||
from serial.tools.list_ports import comports
|
from serial.tools.list_ports import comports
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
Log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SerialStream:
|
class SerialStream:
|
||||||
@ -36,7 +36,7 @@ class SerialStream:
|
|||||||
self._use_threads = sys.platform == 'win32' if use_threads is None else use_threads
|
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 \
|
self._connection = serial.Serial(self._device, **kwargs) if self._use_threads else \
|
||||||
serial.Serial(self._device, timeout=0, write_timeout=0, **kwargs)
|
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
|
||||||
@ -63,10 +63,10 @@ class SerialStream:
|
|||||||
except serial.SerialTimeoutException:
|
except serial.SerialTimeoutException:
|
||||||
n = 0
|
n = 0
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Error writing to stream")
|
Log.exception("Error writing to stream")
|
||||||
raise
|
raise
|
||||||
if n:
|
if n:
|
||||||
LOG.debug(f"Write {data[:n]!r}")
|
Log.debug(f"Write {data[:n]!r}")
|
||||||
self._output_buffer = data[n:]
|
self._output_buffer = data[n:]
|
||||||
else:
|
else:
|
||||||
self._output_buffer += data
|
self._output_buffer += data
|
||||||
@ -84,11 +84,11 @@ class SerialStream:
|
|||||||
except serial.SerialTimeoutException:
|
except serial.SerialTimeoutException:
|
||||||
n = 0
|
n = 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception("Error writing to stream")
|
Log.exception("Error writing to stream")
|
||||||
self._output_buffer_empty.set_exception(e)
|
self._output_buffer_empty.set_exception(e)
|
||||||
self._loop.remove_writer(self._connection)
|
self._loop.remove_writer(self._connection)
|
||||||
if n:
|
if n:
|
||||||
LOG.debug(f"Write {self._output_buffer[:n]!r}")
|
Log.debug(f"Write {self._output_buffer[:n]!r}")
|
||||||
self._output_buffer = self._output_buffer[n:]
|
self._output_buffer = self._output_buffer[n:]
|
||||||
if not self._output_buffer:
|
if not self._output_buffer:
|
||||||
self._loop.remove_writer(self._connection)
|
self._loop.remove_writer(self._connection)
|
||||||
@ -104,7 +104,7 @@ class SerialStream:
|
|||||||
n = self._connection.write(data)
|
n = self._connection.write(data)
|
||||||
finally:
|
finally:
|
||||||
self._output_buffer_lock.acquire()
|
self._output_buffer_lock.acquire()
|
||||||
LOG.debug(f"Write {self._output_buffer[:n]!r}")
|
Log.debug(f"Write {self._output_buffer[:n]!r}")
|
||||||
self._output_buffer = self._output_buffer[n:]
|
self._output_buffer = self._output_buffer[n:]
|
||||||
self._output_buffer_empty = None
|
self._output_buffer_empty = None
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ class SerialStream:
|
|||||||
w = self._connection.in_waiting
|
w = self._connection.in_waiting
|
||||||
if w:
|
if w:
|
||||||
data = self._connection.read(w if n is None else min(n, w))
|
data = self._connection.read(w if n is None else min(n, w))
|
||||||
LOG.debug(f"Read {data!r}")
|
Log.debug(f"Read {data!r}")
|
||||||
return data
|
return data
|
||||||
else:
|
else:
|
||||||
future = self._loop.create_future()
|
future = self._loop.create_future()
|
||||||
@ -130,7 +130,7 @@ class SerialStream:
|
|||||||
w = self._connection.in_waiting
|
w = self._connection.in_waiting
|
||||||
if w and (n is None or n > 1):
|
if w and (n is None or n > 1):
|
||||||
data += self._connection.read(w if n is None else min(n-1, w))
|
data += self._connection.read(w if n is None else min(n-1, w))
|
||||||
LOG.debug(f"Read {data!r}")
|
Log.debug(f"Read {data!r}")
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def readexactly(self, n):
|
async def readexactly(self, n):
|
||||||
|
32
vm.py
32
vm.py
@ -14,7 +14,6 @@ document][VM01B].
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import array
|
import array
|
||||||
import asyncio
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
import logging
|
import logging
|
||||||
@ -23,7 +22,7 @@ import struct
|
|||||||
from utils import DotDict
|
from utils import DotDict
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
Log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
|
class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
|
||||||
@ -46,6 +45,7 @@ class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
|
|||||||
else:
|
else:
|
||||||
raise TypeError("Unrecognised dtype")
|
raise TypeError("Unrecognised dtype")
|
||||||
return bs[:width//8]
|
return bs[:width//8]
|
||||||
|
|
||||||
def decode(self, bs):
|
def decode(self, bs):
|
||||||
if len(bs) < 4:
|
if len(bs) < 4:
|
||||||
bs = bs + bytes(4 - len(bs))
|
bs = bs + bytes(4 - len(bs))
|
||||||
@ -60,6 +60,7 @@ class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
|
|||||||
whole, fraction = map(int, self.dtype[1:].split('.', 1))
|
whole, fraction = map(int, self.dtype[1:].split('.', 1))
|
||||||
value = value / (1 << fraction)
|
value = value / (1 << fraction)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def maximum_value(self):
|
def maximum_value(self):
|
||||||
if '.' in self.dtype:
|
if '.' in self.dtype:
|
||||||
@ -70,6 +71,7 @@ class Register(namedtuple('Register', ['base', 'dtype', 'description'])):
|
|||||||
whole -= 1
|
whole -= 1
|
||||||
n = (1 << (whole+fraction)) - 1
|
n = (1 << (whole+fraction)) - 1
|
||||||
return n / (1 << fraction) if fraction else n
|
return n / (1 << fraction) if fraction else n
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def width(self):
|
def width(self):
|
||||||
if '.' in self.dtype:
|
if '.' in self.dtype:
|
||||||
@ -153,6 +155,9 @@ Registers = DotDict({
|
|||||||
"MasterClockM": Register(0xf8, 'U16', "PLL multiplier (MUL M)"),
|
"MasterClockM": Register(0xf8, 'U16', "PLL multiplier (MUL M)"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# pylama:ignore=E221
|
||||||
|
|
||||||
class TraceMode(IntEnum):
|
class TraceMode(IntEnum):
|
||||||
Analog = 0
|
Analog = 0
|
||||||
Mixed = 1
|
Mixed = 1
|
||||||
@ -172,6 +177,7 @@ class TraceMode(IntEnum):
|
|||||||
Macro = 18
|
Macro = 18
|
||||||
MacroChop = 19
|
MacroChop = 19
|
||||||
|
|
||||||
|
|
||||||
class BufferMode(IntEnum):
|
class BufferMode(IntEnum):
|
||||||
Single = 0
|
Single = 0
|
||||||
Chop = 1
|
Chop = 1
|
||||||
@ -180,6 +186,7 @@ class BufferMode(IntEnum):
|
|||||||
Macro = 4
|
Macro = 4
|
||||||
MacroChop = 5
|
MacroChop = 5
|
||||||
|
|
||||||
|
|
||||||
class DumpMode(IntEnum):
|
class DumpMode(IntEnum):
|
||||||
Raw = 0
|
Raw = 0
|
||||||
Burst = 1
|
Burst = 1
|
||||||
@ -190,6 +197,7 @@ class DumpMode(IntEnum):
|
|||||||
Filter = 6
|
Filter = 6
|
||||||
Span = 7
|
Span = 7
|
||||||
|
|
||||||
|
|
||||||
class SpockOption(IntEnum):
|
class SpockOption(IntEnum):
|
||||||
TriggerInvert = 0x40
|
TriggerInvert = 0x40
|
||||||
TriggerSourceA = 0x04 * 0
|
TriggerSourceA = 0x04 * 0
|
||||||
@ -198,23 +206,28 @@ class SpockOption(IntEnum):
|
|||||||
TriggerTypeSampledAnalog = 0x01 * 0
|
TriggerTypeSampledAnalog = 0x01 * 0
|
||||||
TriggerTypeHardwareComparator = 0x01 * 1
|
TriggerTypeHardwareComparator = 0x01 * 1
|
||||||
|
|
||||||
|
|
||||||
class KitchenSinkA(IntEnum):
|
class KitchenSinkA(IntEnum):
|
||||||
ChannelAComparatorEnable = 0x80
|
ChannelAComparatorEnable = 0x80
|
||||||
ChannelBComparatorEnable = 0x40
|
ChannelBComparatorEnable = 0x40
|
||||||
|
|
||||||
|
|
||||||
class KitchenSinkB(IntEnum):
|
class KitchenSinkB(IntEnum):
|
||||||
AnalogFilterEnable = 0x80
|
AnalogFilterEnable = 0x80
|
||||||
WaveformGeneratorEnable = 0x40
|
WaveformGeneratorEnable = 0x40
|
||||||
|
|
||||||
|
|
||||||
class TraceStatus(IntEnum):
|
class TraceStatus(IntEnum):
|
||||||
Done = 0x00
|
Done = 0x00
|
||||||
Auto = 0x01
|
Auto = 0x01
|
||||||
Wait = 0x02
|
Wait = 0x02
|
||||||
Stop = 0x03
|
Stop = 0x03
|
||||||
|
|
||||||
|
|
||||||
CaptureMode = namedtuple('CaptureMode', ('trace_mode', 'clock_low', 'clock_high', 'clock_divide',
|
CaptureMode = namedtuple('CaptureMode', ('trace_mode', 'clock_low', 'clock_high', 'clock_divide',
|
||||||
'analog_channels', 'sample_width', 'logic_channels', 'buffer_mode'))
|
'analog_channels', 'sample_width', 'logic_channels', 'buffer_mode'))
|
||||||
|
|
||||||
|
|
||||||
CaptureModes = [
|
CaptureModes = [
|
||||||
CaptureMode(TraceMode.Macro, 40, 16384, 1, 1, 2, False, BufferMode.Macro),
|
CaptureMode(TraceMode.Macro, 40, 16384, 1, 1, 2, False, BufferMode.Macro),
|
||||||
CaptureMode(TraceMode.MacroChop, 40, 16384, 1, 2, 2, False, BufferMode.MacroChop),
|
CaptureMode(TraceMode.MacroChop, 40, 16384, 1, 2, 2, False, BufferMode.MacroChop),
|
||||||
@ -241,12 +254,15 @@ class VirtualMachine:
|
|||||||
class Transaction:
|
class Transaction:
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
self._vm = vm
|
self._vm = vm
|
||||||
|
|
||||||
def append(self, cmd):
|
def append(self, cmd):
|
||||||
self._data += cmd
|
self._data += cmd
|
||||||
|
|
||||||
async def __aenter__(self):
|
async def __aenter__(self):
|
||||||
self._data = b''
|
self._data = b''
|
||||||
self._vm._transactions.append(self)
|
self._vm._transactions.append(self)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||||
if self._vm._transactions.pop() != self:
|
if self._vm._transactions.pop() != self:
|
||||||
raise RuntimeError("Mis-ordered transactions")
|
raise RuntimeError("Mis-ordered transactions")
|
||||||
@ -275,7 +291,7 @@ class VirtualMachine:
|
|||||||
if isinstance(cmd, str):
|
if isinstance(cmd, str):
|
||||||
cmd = cmd.encode('ascii')
|
cmd = cmd.encode('ascii')
|
||||||
if not self._transactions:
|
if not self._transactions:
|
||||||
LOG.debug(f"Issue: {cmd!r}")
|
Log.debug(f"Issue: {cmd!r}")
|
||||||
self._writer.write(cmd)
|
self._writer.write(cmd)
|
||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
echo = await self._reader.readexactly(len(cmd))
|
echo = await self._reader.readexactly(len(cmd))
|
||||||
@ -293,7 +309,7 @@ class VirtualMachine:
|
|||||||
index = data.find(b'\r')
|
index = data.find(b'\r')
|
||||||
if index >= 0:
|
if index >= 0:
|
||||||
reply = data[:index]
|
reply = data[:index]
|
||||||
LOG.debug(f"Read reply: {reply!r}")
|
Log.debug(f"Read reply: {reply!r}")
|
||||||
replies.append(reply)
|
replies.append(reply)
|
||||||
data = data[index+1:]
|
data = data[index+1:]
|
||||||
else:
|
else:
|
||||||
@ -305,13 +321,13 @@ class VirtualMachine:
|
|||||||
async def issue_reset(self):
|
async def issue_reset(self):
|
||||||
if self._transactions:
|
if self._transactions:
|
||||||
raise TypeError("Command transaction in progress")
|
raise TypeError("Command transaction in progress")
|
||||||
LOG.debug("Issue reset")
|
Log.debug("Issue reset")
|
||||||
self._writer.write(b'!')
|
self._writer.write(b'!')
|
||||||
await self._writer.drain()
|
await self._writer.drain()
|
||||||
while not (await self._reader.read(1000)).endswith(b'!'):
|
while not (await self._reader.read(1000)).endswith(b'!'):
|
||||||
pass
|
pass
|
||||||
self._reply_buffer = b''
|
self._reply_buffer = b''
|
||||||
LOG.debug("Reset complete")
|
Log.debug("Reset complete")
|
||||||
|
|
||||||
async def set_registers(self, **kwargs):
|
async def set_registers(self, **kwargs):
|
||||||
cmd = ''
|
cmd = ''
|
||||||
@ -319,7 +335,7 @@ class VirtualMachine:
|
|||||||
for base, name in sorted((Registers[name].base, name) for name in kwargs):
|
for base, name in sorted((Registers[name].base, name) for name in kwargs):
|
||||||
register = Registers[name]
|
register = Registers[name]
|
||||||
bs = register.encode(kwargs[name])
|
bs = register.encode(kwargs[name])
|
||||||
LOG.debug(f"{name} = 0x{''.join(f'{b:02x}' for b in reversed(bs))}")
|
Log.debug(f"{name} = 0x{''.join(f'{b:02x}' for b in reversed(bs))}")
|
||||||
for i, byte in enumerate(bs):
|
for i, byte in enumerate(bs):
|
||||||
if cmd:
|
if cmd:
|
||||||
cmd += 'z'
|
cmd += 'z'
|
||||||
@ -425,5 +441,3 @@ class VirtualMachine:
|
|||||||
|
|
||||||
async def issue_write_eeprom(self):
|
async def issue_write_eeprom(self):
|
||||||
await self.issue(b'w')
|
await self.issue(b'w')
|
||||||
|
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user