From 947b1be4d6cfe03e36384bb69af3fe577732cfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 16 Jun 2018 21:22:48 +0200 Subject: [PATCH] code import --- .gitignore | 2 + UNITS.INI | 130 ++++++++++++++++++++++++++++++++++ gex | 1 + main.py | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 UNITS.INI create mode 120000 gex create mode 100644 main.py diff --git a/.gitignore b/.gitignore index 6a18ad4..6c0219b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ __pycache__/ # C extensions *.so +.idea/ + # Distribution / packaging .Python env/ diff --git a/UNITS.INI b/UNITS.INI new file mode 100644 index 0000000..ce23b06 --- /dev/null +++ b/UNITS.INI @@ -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 diff --git a/gex b/gex new file mode 120000 index 0000000..c49c075 --- /dev/null +++ b/gex @@ -0,0 +1 @@ +../gex_client_py/gex \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..b3bdc88 --- /dev/null +++ b/main.py @@ -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() + +