For those who haven't used it, the built in Frequency Response Analysis tool on Keysight scopes is a little clunky and slow. Along with a lack of control over the parameters it can fail to scale well and just give up on some runs. While working on some other automation it struck me how fast these scopes respond to visa commands, so an afternoon later we have a reimplementation of the FRA except from a host computer.
import visa
import usb
import numpy as np
f_min = 100;
f_max = 10e6;
f_points_decade = 50;
f_vector = np.append(10**np.arange(np.log10(f_min),np.log10(f_max),1/float(f_points_decade)),f_max)
gain_vector = np.zeros_like(f_vector)
phase_vector = np.zeros_like(f_vector)
input_scales = [500e-6,1e-3,2e-3,5e-3,10e-3,20e-3,50e-3,100e-3,200e-3,500e-3,1e0,2e0,5e0,10e0]
input_scale = 10
probe_scale = 10
#reset usb to recover from unclosed resource
#dev = usb.core.find(idVendor=0x2a8d, idProduct=0x1797)
#print(dev)
#dev.reset()
rm = visa.ResourceManager()
print(rm.list_resources())
o_scope = rm.open_resource(rm.list_resources()[0])
#print(o_scope.query('*IDN?'))
o_scope.write('ACQuire:TYPE HRESolution')
o_scope.write('TIMebase:POSition 0E0;RANGe 100E-3')
o_scope.write('WGEN:FUNCtion SINusoid;OUTPut ON')
o_scope.write('MEASure:CLEar')#;VRMS CYCLe,AC,CHANnel1;VRMS CYCLe,AC,CHANnel2;PHASe CHANnel1,CHANnel2')
o_scope.write('CHANnel1:COUPling AC;PROBe 1E0;OFFSet 0 V;SCALe '+("{:.0e}".format(input_scales[input_scale])))
o_scope.write('CHANnel2:COUPling AC;PROBe 1E0;OFFSet 0 V;SCALe '+("{:.0e}".format(input_scales[probe_scale])))
o_scope.write(':RUN')
for i,frequency in enumerate(f_vector):
o_scope.write('WGEN:FREQuency '+("{:.3e}".format(frequency)))
o_scope.write('TIMebase:POSition 0E0;RANGe '+("{:.3e}".format(4/frequency)))
if (frequency > 10e3):
o_scope.write('ACQuire:TYPE AVERage;COUNt 128')
o_scope.write(':DIGitize')
data_good = False
while (data_good == False):
data_good = True # think positive!
v_source = o_scope.query_ascii_values('MEASure:VRMS? DISPlay,AC,CHANnel1')[0]
v_probe = o_scope.query_ascii_values('MEASure:VRMS? DISPlay,AC,CHANnel2')[0]
if (v_source > 2*input_scales[input_scale]):
if (input_scale == (len(input_scales)-1)):
print('Data Clipped at max range!')
else:
input_scale = input_scale + 1
o_scope.write('CHANnel1:SCALe '+("{:.0e}".format(input_scales[input_scale])))
data_good = False
else:
if (v_source*2 < input_scales[input_scale]) & (input_scale != 0):
input_scale = input_scale - 1
o_scope.write('CHANnel1:SCALe '+("{:.0e}".format(input_scales[input_scale])))
data_good = False
if (v_source > 2*input_scales[probe_scale]):
if (probe_scale == (len(input_scales)-1)):
print('Data Clipped at max range!')
else:
probe_scale = probe_scale + 1
o_scope.write('CHANnel2:SCALe '+("{:.0e}".format(input_scales[probe_scale])))
data_good = False
else:
if (v_source*2 < input_scales[probe_scale]) & (probe_scale != 0):
probe_scale = probe_scale - 1
o_scope.write('CHANnel2:SCALe '+("{:.0e}".format(input_scales[probe_scale])))
data_good = False
if (data_good == True):
d_phase = o_scope.query_ascii_values('MEASure:PHASe? CHANnel1,CHANnel2')[0]
if (d_phase > 1e6):
data_good = False
gain_vector[i] = v_probe/v_source
phase_vector[i] = d_phase
o_scope.close()
Wonder of wonders, its 3 times faster than using the scope its self and you end up with the data neatly stored on your computer ready for use. Its not as accurate on the phase measurement (the built in might be using a separate optimised capture window to get that measurement) but does better for gain using averaging for higher frequencies. With the ability to customise this for your specific needs you can optimise it easily.
Future work for an FFT based FRA from the step response could be very interesting.