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

Revised analog params; move VM-specific IO routines into vm.py; remove EEPROM params code (doesn't work); a few lines of documentation here and there

This commit is contained in:
Jonathan Hogg
2017-07-11 11:33:49 +01:00
parent 942c7e7dbc
commit be7e7e97c1
3 changed files with 80 additions and 52 deletions

View File

@ -2,10 +2,10 @@
import argparse import argparse
import asyncio import asyncio
from collections import namedtuple
import logging import logging
import math import math
import os import os
import struct
import sys import sys
import streams import streams
@ -25,6 +25,8 @@ class Scope(vm.VirtualMachine):
PARAMS_MAGIC = 0xb0b2 PARAMS_MAGIC = 0xb0b2
AnalogParams = namedtuple('AnalogParams', ['d', 'f', 'b', 'scale', 'offset'])
@classmethod @classmethod
async def connect(cls, device=None): async def connect(cls, device=None):
if device is None: if device is None:
@ -52,16 +54,15 @@ class Scope(vm.VirtualMachine):
self.awg_sample_buffer_size = 1024 self.awg_sample_buffer_size = 1024
self.awg_minimum_clock = 33 self.awg_minimum_clock = 33
self.awg_maximum_voltage = 3.33 self.awg_maximum_voltage = 3.33
self.analog_params = (20.164, -5.2470, 299.00, 18.472, 0.40827) self.analog_params = self.AnalogParams(20.2, -4.9, 300, 18.333, -7.517)
self.analog_offsets = {'A': -0.011924, 'B': 0.011924} self.analog_offsets = {'A': 0, 'B': 0}
self.analog_min = -5.7 self.analog_min = -5.7
self.analog_max = 8 self.analog_max = 8
self.capture_clock_period = 25e-9 self.capture_clock_period = 25e-9
self.capture_buffer_size = 12<<10 self.capture_buffer_size = 12<<10
self.timeout_clock_period = 6.4e-6 self.timeout_clock_period = 6.4e-6
self.trigger_low = -7.517 else:
self.trigger_high = 10.816 raise RuntimeError(f"Unsupported scope revision: {revision}")
# await self.load_params() XXX switch this off until I understand EEPROM better
self._awg_running = False self._awg_running = False
Log.info(f"Initialised scope, revision: {revision}") Log.info(f"Initialised scope, revision: {revision}")
@ -73,32 +74,14 @@ class Scope(vm.VirtualMachine):
__del__ = close __del__ = close
async def load_params(self):
params = []
for i in range(struct.calcsize('<H8fH')):
params.append(await self.read_eeprom(i+70))
params = struct.unpack('<H8fH', bytes(params))
if params[0] == self.PARAMS_MAGIC and params[-1] == self.PARAMS_MAGIC:
self.analog_params = tuple(params[1:7])
self.analog_offsets['A'] = params[8]
self.analog_offsets['B'] = params[9]
async def save_params(self):
params = struct.pack('<H8fH', self.PARAMS_MAGIC, *self.analog_params,
self.analog_offsets['A'], self.analog_offsets['B'], self.PARAMS_MAGIC)
for i, byte in enumerate(params):
await self.write_eeprom(i+70, byte)
def calculate_lo_hi(self, low, high, params=None): def calculate_lo_hi(self, low, high, params=None):
if params is None: params = self.analog_params if params is None else self.AnalogParams(*params)
params = self.analog_params l = (low - params.offset) / params.scale
d, f, b, scale, offset = params h = (high - params.offset) / params.scale
l = low / scale + offset al = params.d + params.f * (2*l-1)**2
h = high / scale + offset ah = params.d + params.f * (2*h-1)**2
al = d + f * (2*l - 1)**2 dl = l + (2*l-h)*al/params.b
ah = d + f * (2*h - 1)**2 dh = h + (2*h-l-1)*ah/params.b
dl = (l*(2*al + b) - al*h) / b
dh = (h*(2*ah + b) - ah*(l + 1)) / b
return dl, dh return dl, dh
async def capture(self, channels=['A'], trigger=None, trigger_level=None, trigger_type='rising', hair_trigger=False, async def capture(self, channels=['A'], trigger=None, trigger_level=None, trigger_type='rising', hair_trigger=False,
@ -183,7 +166,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
trigger_level = (trigger_level - self.trigger_low) / (self.trigger_high - self.trigger_low) trigger_level = (trigger_level - self.analog_params.offset) / self.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
@ -245,13 +228,9 @@ class Scope(vm.VirtualMachine):
DumpChan=dump_channel, DumpCount=asamples, DumpRepeat=1, DumpSend=1, DumpSkip=0) DumpChan=dump_channel, DumpCount=asamples, DumpRepeat=1, DumpSend=1, DumpSkip=0)
await self.issue_program_spock_registers() await self.issue_program_spock_registers()
await self.issue_analog_dump_binary() await self.issue_analog_dump_binary()
data = await self._reader.readexactly(asamples * capture_mode.sample_width)
value_multiplier, value_offset = (1, 0) if raw else ((high-low), low+self.analog_offsets[channel]) value_multiplier, value_offset = (1, 0) if raw else ((high-low), low+self.analog_offsets[channel])
if capture_mode.sample_width == 2: data = await self.read_analog_samples(asamples, capture_mode.sample_width)
data = struct.unpack(f'>{asamples}h', data) data = (value*value_multiplier + value_offset for value in data)
data = ((value/65536+0.5)*value_multiplier + value_offset for value in data)
else:
data = ((value/256)*value_multiplier + value_offset for value in data)
ts = (t*self.capture_clock_period for t in range(start_timestamp+dump_channel*ticks*clock_scale, timestamp, ts = (t*self.capture_clock_period for t in range(start_timestamp+dump_channel*ticks*clock_scale, timestamp,
ticks*clock_scale*len(analog_channels))) ticks*clock_scale*len(analog_channels)))
traces[channel] = dict(zip(ts, data)) traces[channel] = dict(zip(ts, data))
@ -262,7 +241,7 @@ class Scope(vm.VirtualMachine):
DumpMode=vm.DumpMode.Raw, DumpChan=128, DumpCount=nsamples, DumpRepeat=1, DumpSend=1, DumpSkip=0) DumpMode=vm.DumpMode.Raw, DumpChan=128, DumpCount=nsamples, DumpRepeat=1, DumpSend=1, DumpSkip=0)
await self.issue_program_spock_registers() await self.issue_program_spock_registers()
await self.issue_analog_dump_binary() await self.issue_analog_dump_binary()
data = await self._reader.readexactly(nsamples) data = await self.read_logic_samples(nsamples)
ts = [t*self.capture_clock_period for t in range(start_timestamp, timestamp, ticks*clock_scale)] ts = [t*self.capture_clock_period for t in range(start_timestamp, timestamp, ticks*clock_scale)]
for i in logic_channels: for i in logic_channels:
mask = 1<<i mask = 1<<i
@ -326,7 +305,7 @@ class Scope(vm.VirtualMachine):
with self.transaction(): with self.transaction():
self.set_registers(Address=0, Size=self.awg_wavetable_size) self.set_registers(Address=0, Size=self.awg_wavetable_size)
self.issue_wavetable_read() self.issue_wavetable_read()
return list(self.read_exactly(self.awg_wavetable_size)) return list(self.wavetable_read_bytes(self.awg_wavetable_size))
async def read_eeprom(self, address): async def read_eeprom(self, address):
async with self.transaction(): async with self.transaction():
@ -344,6 +323,7 @@ class Scope(vm.VirtualMachine):
async def calibrate(self, n=32): async def calibrate(self, n=32):
import numpy as np import numpy as np
from scipy.optimize import least_squares from scipy.optimize import least_squares
global items
items = [] items = []
await self.start_generator(frequency=1000, waveform='square') await self.start_generator(frequency=1000, waveform='square')
i = 0 i = 0
@ -362,7 +342,7 @@ class Scope(vm.VirtualMachine):
analog_range = self.awg_maximum_voltage / ((Amax + Bmax)/2 - zero) analog_range = self.awg_maximum_voltage / ((Amax + Bmax)/2 - zero)
analog_low = -zero * analog_range analog_low = -zero * analog_range
analog_high = analog_low + analog_range analog_high = analog_low + analog_range
offset = (Azero - Bzero) / 2 * analog_range offset = ((Amax - Bmax) + (Azero - Bzero))/2 * analog_range
items.append((analog_low, analog_high, low, high, offset)) items.append((analog_low, analog_high, low, high, offset))
i += 1 i += 1
await self.stop_generator() await self.stop_generator()
@ -370,11 +350,16 @@ class Scope(vm.VirtualMachine):
def f(params, analog_low, analog_high, low, high): def f(params, analog_low, analog_high, low, high):
lo, hi = self.calculate_lo_hi(analog_low, analog_high, params) lo, hi = self.calculate_lo_hi(analog_low, analog_high, params)
return np.sqrt((low - lo) ** 2 + (high - hi) ** 2) return np.sqrt((low - lo) ** 2 + (high - hi) ** 2)
result = least_squares(f, self.analog_params, args=items.T[:4], bounds=([0, -np.inf, 250, 0, 0], [np.inf, np.inf, 350, np.inf, np.inf])) result = least_squares(f, self.analog_params, args=items.T[:4], max_nfev=10000,
bounds=([0, -np.inf, 285, 17.4, -7.89], [np.inf, np.inf, 315, 19.2, -7.14]))
if result.success in range(1, 5): if result.success in range(1, 5):
self.analog_params = tuple(result.x) Log.info(f"Calibration succeeded: {result.success}")
offset = items[:,4].mean() self.analog_params = params = self.AnalogParams(*result.x)
self.analog_offsets = {'A': -offset, 'B': +offset} Log.info(f"Analog parameters: d={params.d:.1f}Ω f={params.f:.2f}Ω b={params.b:.1f}Ω scale={params.scale:.3f}V offset={params.offset:.3f}V")
offsets = items[:,4]
offset = offsets.mean()
Log.info(f"Mean A-B offset is {offset*1000:.1f}mV (+/- {100*offsets.std()/offset:.1f}%)")
self.analog_offsets = {'A': -offset/2, 'B': +offset/2}
else: else:
Log.warning(f"Calibration failed: {result.message}") Log.warning(f"Calibration failed: {result.message}")
return result.success return result.success

View File

@ -1,3 +1,9 @@
"""
streams
=======
Package for asynchronous serial IO.
"""
import asyncio import asyncio
import logging import logging

51
vm.py
View File

@ -1,4 +1,18 @@
"""
vm
==
Package capturing BitScope VM specification, including registers, enumerations, flags,
commands and logic for encoding and decoding virtual machine instructions and data.
All names and descriptions copyright BitScope and taken from their [VM specification
document][VM01B].
[VM01B]: https://docs.google.com/document/d/1cZNRpSPAMyIyAvIk_mqgEByaaHzbFTX8hWglAMTlnHY
"""
import asyncio import asyncio
from collections import namedtuple from collections import namedtuple
from enum import IntEnum from enum import IntEnum
@ -86,19 +100,19 @@ Registers = {
class TraceMode(IntEnum): class TraceMode(IntEnum):
Analog = 0 Analog = 0
AnalogFast = 4
AnalogShot = 11
Mixed = 1 Mixed = 1
AnalogChop = 2
MixedChop = 3
AnalogFast = 4
MixedFast = 5 MixedFast = 5
AnalogFastChop = 6
MixedFastChop = 7
AnalogShot = 11
MixedShot = 12 MixedShot = 12
LogicShot = 13
Logic = 14 Logic = 14
LogicFast = 15 LogicFast = 15
LogicShot = 13
AnalogChop = 2
AnalogFastChop = 6
AnalogShotChop = 16 AnalogShotChop = 16
MixedChop = 3
MixedFastChop = 7
MixedShotChop = 17 MixedShotChop = 17
Macro = 18 Macro = 18
MacroChop = 19 MacroChop = 19
@ -319,6 +333,24 @@ class VirtualMachine:
async def issue_triggered_trace(self): async def issue_triggered_trace(self):
await self.issue(b'D') await self.issue(b'D')
async def read_analog_samples(self, n, sample_width):
if self._transactions:
raise TypeError("Command transaction in progress")
if sample_width == 2:
data = await self._reader.readexactly(2 * n)
data = struct.unpack(f'>{n}h', data)
return [value/65536 + 0.5 for value in data]
elif sample_width == 1:
data = await self._reader.readexactly(n)
return [value/256 for value in data]
else:
raise ValueError(f"Bad sample width: {sample_width}")
async def read_logic_samples(self, n):
if self._transactions:
raise TypeError("Command transaction in progress")
return await self._reader.readexactly(n)
async def issue_cancel_trace(self): async def issue_cancel_trace(self):
await self.issue(b'K') await self.issue(b'K')
@ -331,6 +363,11 @@ class VirtualMachine:
async def issue_wavetable_read(self): async def issue_wavetable_read(self):
await self.issue(b'R') await self.issue(b'R')
async def wavetable_read_bytes(self, n):
if self._transactions:
raise TypeError("Command transaction in progress")
return await self._reader.readexactly(n)
async def wavetable_write_bytes(self, bs): async def wavetable_write_bytes(self, bs):
cmd = '' cmd = ''
last_byte = None last_byte = None