code import
This commit is contained in:
@@ -7,6 +7,8 @@ __pycache__/
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
.idea/
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
## UNITS.INI
|
||||
## GEX v1.0.0 on STM32F072-HUB
|
||||
## built Jun 15 2018 at 13:45:28
|
||||
|
||||
[UNITS]
|
||||
# Create units by adding their names next to a type (e.g. DO=A,B),
|
||||
# remove the same way. Reload to update the unit sections below.
|
||||
|
||||
# Digital output
|
||||
DO=areset
|
||||
# Digital input with triggers
|
||||
DI=psign
|
||||
# Neopixel RGB LED strip
|
||||
NPX=
|
||||
# I2C master
|
||||
I2C=
|
||||
# SPI master
|
||||
SPI=spi
|
||||
# Serial port
|
||||
USART=
|
||||
# 1-Wire master
|
||||
1WIRE=
|
||||
# Analog/digital converter
|
||||
ADC=adc
|
||||
# Shift register driver (595, 4094)
|
||||
SIPO=
|
||||
# Frequency and pulse measurement
|
||||
FCAP=fcap
|
||||
# Capacitive touch sensing
|
||||
TOUCH=
|
||||
# Simple PWM output
|
||||
PWMDIM=
|
||||
# Two-channel analog output with waveforms
|
||||
DAC=
|
||||
|
||||
[DO:areset@1]
|
||||
# Port name
|
||||
port=A
|
||||
# Pins (comma separated, supports ranges)
|
||||
pins=8
|
||||
# Initially high pins
|
||||
initial=8
|
||||
# Open-drain pins
|
||||
open-drain=
|
||||
|
||||
[DI:psign@2]
|
||||
# Port name
|
||||
port=A
|
||||
# Pins (comma separated, supports ranges)
|
||||
pins=3
|
||||
# Pins with pull-up
|
||||
pull-up=
|
||||
# Pins with pull-down
|
||||
pull-down=
|
||||
|
||||
# Trigger pins activated by rising/falling edge
|
||||
trig-rise=
|
||||
trig-fall=
|
||||
# Trigger pins auto-armed by default
|
||||
auto-trigger=
|
||||
# Triggers hold-off time (ms)
|
||||
hold-off=100
|
||||
|
||||
[SPI:spi@3]
|
||||
# Peripheral number (SPIx)
|
||||
device=1
|
||||
# Pin mappings (SCK,MISO,MOSI)
|
||||
# SPI1: (0) A5,A6,A7 (1) B3,B4,B5
|
||||
# SPI2: (0) B13,B14,B15
|
||||
remap=0
|
||||
|
||||
# Prescaller: 2,4,8,...,256
|
||||
prescaller=64
|
||||
# Clock polarity: 0,1 (clock idle level)
|
||||
cpol=1
|
||||
# Clock phase: 0,1 (active edge, 0-first, 1-second)
|
||||
cpha=1
|
||||
# Transmit only, disable MISO
|
||||
tx-only=N
|
||||
# Bit order (LSB or MSB first)
|
||||
first-bit=MSB
|
||||
|
||||
# SS port name
|
||||
port=A
|
||||
# SS pins (comma separated, supports ranges)
|
||||
pins=14
|
||||
|
||||
[ADC:adc@4]
|
||||
# Enabled channels, comma separated
|
||||
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
||||
# A0 A1 A2 A3 A4 A5 A6 A7 B0 B1 C0 C1 C2 C3 C4 C5 Tsens Vref
|
||||
channels=1-2, 17
|
||||
|
||||
# Sampling time (0-7)
|
||||
sample_time=2
|
||||
# Sampling frequency (Hz)
|
||||
frequency=10000
|
||||
|
||||
# Sample buffer size
|
||||
# - shared by all enabled channels
|
||||
# - defines the maximum pre-trigger size (divide by # of channels)
|
||||
# - captured data is sent in half-buffer chunks
|
||||
# - buffer overrun aborts the data capture
|
||||
buffer_size=256
|
||||
|
||||
# Enable continuous sampling with averaging
|
||||
# Caution: This can cause DAC output glitches
|
||||
averaging=Y
|
||||
# Exponential averaging coefficient (permil, range 0-1000 ~ 0.000-1.000)
|
||||
# - used formula: y[t]=(1-k)*y[t-1]+k*u[t]
|
||||
# - not available when a capture is running
|
||||
avg_factor=800
|
||||
|
||||
[FCAP:fcap@5]
|
||||
# Signal input pin - one of:
|
||||
# Full support: A0, A5, A15
|
||||
# Indirect only: A1, B3
|
||||
pin=A0
|
||||
|
||||
# Active level or edge (0-low,falling; 1-high,rising)
|
||||
active-level=1
|
||||
# Input filtering (0-15)
|
||||
input-filter=0
|
||||
# Pulse counter pre-divider (1,2,4,8)
|
||||
direct-presc=1
|
||||
# Pulse counting interval (ms)
|
||||
direct-time=1000
|
||||
|
||||
# Mode on startup: N-none, I-indirect, D-direct, F-free count
|
||||
initial-mode=N
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/bin/env python3
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
|
||||
import gex
|
||||
import time
|
||||
|
||||
class ADG:
|
||||
def __init__(self, client:gex.Client):
|
||||
self.client = client
|
||||
self.spi = gex.SPI(client, 'spi')
|
||||
self.pb = gex.PayloadBuilder(endian='big')
|
||||
|
||||
def write_word(self, u16):
|
||||
self.pb.reset()
|
||||
self.pb.u16(u16)
|
||||
self.spi.write(0, self.pb.close())
|
||||
|
||||
def initialize(self):
|
||||
# enable 28-bit writes, reset registers, configured for SINE output
|
||||
self.write_word(0x2100)
|
||||
self.write_word(0xC000) # phase word
|
||||
self.set_frequency(0) # freq 0
|
||||
self.write_word(0x2000)
|
||||
|
||||
def set_frequency(self, hz):
|
||||
word = round(hz * 10.73741824)
|
||||
self.write_word(0x4000 | (word & 0x0003FFF))
|
||||
self.write_word(0x4000 | (word & 0xFFFC000)>>14)
|
||||
|
||||
def wfm_dc(self):
|
||||
self.write_word(0x2100)
|
||||
self.write_word(0x2000)
|
||||
self.set_frequency(0)
|
||||
|
||||
def wfm_sine(self, freq=None):
|
||||
self.write_word(0x2000)
|
||||
if freq is not None:
|
||||
self.set_frequency(freq)
|
||||
|
||||
with gex.Client(gex.TrxRawUSB()) as client:
|
||||
# ===============================================
|
||||
|
||||
# Delay between adjusting input and starting the measurement.
|
||||
# Should be several multiples of the time constant
|
||||
settling_time_s = (4700*100e-9)*10
|
||||
|
||||
# max change in DB between samples to detect faulty measurements that need to be repeated
|
||||
max_allowed_shift_db = 5
|
||||
# db shift compensation (spread through the frequency sweep to adjust for different slopes)
|
||||
allowed_shift_compensation = -4.5
|
||||
|
||||
|
||||
if False:
|
||||
# highpass filter example (corner 340 Hz)
|
||||
settling_time_s = (4700*100e-9)*10
|
||||
max_allowed_shift_db = 5
|
||||
allowed_shift_compensation = -4.5
|
||||
|
||||
|
||||
if True:
|
||||
# lowpass filter example (corner 340 Hz)
|
||||
settling_time_s = (4700*100e-9)*10
|
||||
max_allowed_shift_db = 1
|
||||
allowed_shift_compensation = 1.2
|
||||
|
||||
|
||||
# Frequency sweep parameters
|
||||
f_0 = 5
|
||||
f_1 = 5000
|
||||
f_step = 15
|
||||
|
||||
# Retry on failure
|
||||
retry_count = 5
|
||||
retry_delay_s = settling_time_s*10
|
||||
|
||||
# Initial sample granularity
|
||||
samples_per_period = 60
|
||||
capture_periods = 10
|
||||
|
||||
# Parameters for automatic params adjustment
|
||||
max_allowed_sample_rate = 36000
|
||||
max_allowed_nr_periods = 80
|
||||
min_samples_per_period = 4
|
||||
|
||||
# ===============================================
|
||||
|
||||
allowed_shift_compensation /= (f_1 - f_0) / f_step
|
||||
|
||||
adc = gex.ADC(client, 'adc')
|
||||
|
||||
gen = ADG(client)
|
||||
gen.initialize()
|
||||
|
||||
|
||||
table = []
|
||||
|
||||
last_db = None
|
||||
for f in range(f_0, f_1, f_step):
|
||||
#dac.set_frequency(1, f)
|
||||
gen.set_frequency(f)
|
||||
|
||||
max_allowed_shift_db += allowed_shift_compensation
|
||||
|
||||
# Adjust measurement parameters
|
||||
while True:
|
||||
desiredf = f*samples_per_period
|
||||
if desiredf > max_allowed_sample_rate:
|
||||
oldspp = samples_per_period
|
||||
samples_per_period = math.ceil(samples_per_period * 0.9)
|
||||
|
||||
if samples_per_period == oldspp:
|
||||
samples_per_period -= 1
|
||||
|
||||
if samples_per_period <= min_samples_per_period:
|
||||
samples_per_period = min_samples_per_period
|
||||
break
|
||||
|
||||
if capture_periods < max_allowed_nr_periods:
|
||||
capture_periods = math.ceil(capture_periods * 1.1)
|
||||
|
||||
continue
|
||||
break
|
||||
|
||||
num_samples = samples_per_period * capture_periods
|
||||
print("\x1b[90mCap %d samples at %d Hz (samples per period %d, periods: %d, max db shift %f)\x1b[0m" % (
|
||||
num_samples, desiredf, samples_per_period, capture_periods, max_allowed_shift_db))
|
||||
|
||||
adc.set_sample_rate(desiredf)
|
||||
|
||||
last_db_in_fail = None
|
||||
suc = False
|
||||
for i in range(retry_count):
|
||||
time.sleep(settling_time_s if i == 0 else retry_delay_s)
|
||||
|
||||
t = None
|
||||
|
||||
samples = adc.capture(num_samples)
|
||||
ar = np.array(samples, dtype=float)
|
||||
|
||||
try:
|
||||
t = np.reshape(ar, [num_samples, 2])
|
||||
except ValueError:
|
||||
print("\x1b[31mCorrupt capture, repeating - try %d\x1b[0m" % (i+1))
|
||||
continue
|
||||
|
||||
y1 = np.max(t[:,0]) - np.min(t[:,0])
|
||||
y2 = np.max(t[:,1]) - np.min(t[:,1])
|
||||
gain_raw = y2/y1
|
||||
gain_db = 20*math.log10(gain_raw)
|
||||
|
||||
# check feasibility
|
||||
if last_db is not None:
|
||||
dbdelta = abs(last_db - gain_db)
|
||||
if dbdelta > max_allowed_shift_db:
|
||||
print("\x1b[31mGlitch detected (dB delta %f), repeating - try %d\x1b[0m" % (dbdelta, i+1))
|
||||
last_db_in_fail = gain_db
|
||||
continue
|
||||
|
||||
last_db = gain_db
|
||||
|
||||
avr1 = np.average(t[:,0])
|
||||
avr2 = np.average(t[:,0])
|
||||
|
||||
aa = np.subtract(t[:,0], avr1)
|
||||
bb = np.subtract(t[:, 1], avr2)
|
||||
|
||||
phaseoffset = (math.acos((np.dot(aa,bb))/(np.linalg.norm(aa) * np.linalg.norm(bb))) / math.pi) * -180
|
||||
|
||||
table.append(f)
|
||||
table.append(gain_db)
|
||||
table.append(phaseoffset)
|
||||
print("f %f Hz ... Gain %f dB ... Phase %f °" % (f, gain_db, phaseoffset))
|
||||
|
||||
suc = True
|
||||
break
|
||||
if not suc:
|
||||
last_db = last_db_in_fail
|
||||
|
||||
gen.wfm_dc()
|
||||
|
||||
t = np.reshape(np.array(table), [int(len(table) / 3), 3])
|
||||
|
||||
freqs = t[:, 0]
|
||||
gains = t[:, 1]
|
||||
phases = t[:, 2]
|
||||
|
||||
plt.figure()
|
||||
plt.ylabel('Gain (dB)')
|
||||
plt.xlabel('Frequency (Hz)')
|
||||
plt.semilogx(freqs, gains) # Bode magnitude plot
|
||||
plt.grid()
|
||||
|
||||
plt.figure()
|
||||
plt.ylabel('Phase (deg)')
|
||||
plt.xlabel('Frequency (Hz)')
|
||||
plt.semilogx(freqs, phases) # Bode phase plot
|
||||
plt.grid()
|
||||
plt.show()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user