The quest to achieve frequency response measurements in my home lab began around one and half years ago, with this Rohm buck converter roadtest. After that, I made a followup trying to measure the loop response on that specific board but I got mixed results. With passive circuits (RC filters) it worked decently but it was kind of useless for measuring anything more complex.
Then, as usual, life went on and many other things happened. I changed jobs, bought an apartment and moved in with my lovely girlfriend. All of this was great but finding the time to work on hobby projects became a luxury so I had to put this goal on hold. However, that time has come again! The timing of this spring clean competition is perfect for giving me the push I need to finish this long-overdue project!
How I left things up
Last time I left this project only partially functional, but I had a great conversation with michaelkellett on how to improve my setup: moving the post processing to the pc, by acquiring many more periods and using dft to extract amplitude and phase information. This is exactly what I'm going to do now.
The measurement setup
The measurement setup remained unchanged from the previous attempt, and consists of a signal generator to stimulate the DUT, an isolation transformer, the scope to acquire input and out waveforms and a PC to coordinate the instruments and to elaborate the data.
This is the same setup, just with augmented reality

The script
This is the main point of the project. I started with the old script and evolved it, fixing along the way some minor bugs I left last time to keep things more interesting . The following flow chart illustrates what I have in mind.

To avoid navigating through a km long script to modify a couple parameters, I decided to leave most of the instruments setup on the instruments themself, relying on the script only for essential parameters (such as number of points per acquisition or signal amplitude). Once manual preparation is complete, the script can be launched and it will control the signal generator and the scope, adapting horizontal and vertical scale as required.
I decided to avoid real time graph plotting and opted for a simpler post processing once all the data has been gathered.
It's far from perfect, but after a few iterations I managed to produce something that works reasonably well (more on that later).
/*NOTE:
IMPORTANT: needs scilab 6.0.x to run! does not work on newer versions
Program to control in conjunction a scope and a function
generator to create a bode plot.
Needs visa toolbox, use atomsInstall("visa") to install it
If the toolbox does not start automatically, you have to load it manually!
Setup done by script set up section
For "advanced" set up (as trigger noise reject ecc...) it must be done
directly on the scope, not inserted in this script
IMPORTANT:
This version of the program uses scilab to calculate the fft instead of
relying only on amplitude and phase measured by the scope
Known ISSUES:
*/
xdel(winsid());//note: xdel will be deprecated in future versions
clear;
clc;
disp("Script started");
//----------------------SCRIPT SET UP PARAMETERS------------------------------
NaN = %nan;
//select between linear and log frequency sweep, plot will be affected as well
//choose one or the other
//sweep_type = "LIN";
sweep_type = "LOG";
//select between linear and log amplitude plot
//choose one or the other
//y_type = "LIN";
y_type = "LOG";
//start and stop frequency
f_start = 1000;
f_stop = 100000;
//number of points
sample_n = 20;
//delays (in milliseconds)
wait_meas_time = 4000;//default value, will be adjusted for every measure
meas_safety_factor = 2.5;
wait_set_time = 300;
wait_channel_vscale_setup = 2000;//allow for AC coupling stabilization
//max number of measure attempts for each frequency point
max_measure_attempts = 8;
//target number of periods to capture
//----------------SCOPE PARAMETERS
//scope address
scope_addr = "USB0::0x1AB1::0x04CE::DS1ZA211403170::INSTR";//My scope address
//scope channels
scope_in_ch = "1";//dut input signal
scope_out_ch = "2";//dut output signal
scope_unused_chA = "3";//unused channel
scope_unused_chB = "4";//unused channel
//measurement decision parameters
n_of_period = 50;//minimum number of periods to be acquired for one measure
v_up_th = 0.95;//threshold to increase vertical scale
v_down_th = 0.35;//threshold to decrease vertical scale
//scope vertical options
vscale_opt = [0.001;0.002;0.005;0.01;0.02;0.05;0.1;0.2;0.5;1;2;5];//option list. 5mV, 2mV and 1mV only for x1 probes!
vscale_opt_n = size(vscale_opt);//get vector size
actual_vscale_in_opt = 11;//index for the in channel actual option, start at 2V/div
actual_vscale_out_opt = 11;//index for the out channel actual option, start at 2V/div
vscale_div = 8;//number of divisions
//scope horizontal options
hscale_opt = [1e-6;2*1e-6;5*1e-6;1e-5;2*1e-5;5*1e-5;1e-4;2*1e-4;5*1e-4;0.001;0.002;0.005;0.01;0.02;0.05;0.1;0.2];//option list
hscale_opt_n = size(hscale_opt);//get vector size
actual_hscale_opt = hscale_opt_n(1,1);//index for the actual option
hscale_div = 12;//number of divisions
//scope memory depth
mem_depth = 6000;//std value for rigol scope using two channels, do not exceed 15625!!! this is due to ASCII double formatting
//----------------SIGGEN PARAMETERS
//function generator address
siggen_addr = "USB0::0xF4EC::0x1102::SDG2XFBX7R1988::INSTR";//My signal generator address
//siggen channel
siggen_out_ch = "C1";//output channel
//output voltage peak to peak
out_amplitude = 1;//in V, peak to peak, HiZ (by default, can be changed in the instrument)
//------------------------DATA MEMORY PREPARATION-----------------------------
f_vect = zeros(sample_n,1);//frequency vector
phase_in_vect = zeros(sample_n,1);//input phase data vector
phase_out_vect = zeros(sample_n,1);//input phase data vector
gain_vect = zeros(sample_n,1);//gain data vector
phase_vect = zeros(sample_n,1);//phase data vector
//choose frequency spacing
if sweep_type == "LIN" then
f_vect = linspace(f_start,f_stop,sample_n);
else
f_vect = logspace(log10(f_start),log10(f_stop),sample_n);
end
f_vect = f_vect';//traspose the vector
//-------------------------CHECK INSTRUMENTS---------------------------------
[status,deviceAddr] = findAllInstruments();//use to find all instruments
[nrow,ncol] = size(deviceAddr);//get how many instruments are connected
//check for connected scope
flag = 1;
for i = 1:nrow
if deviceAddr(i) == scope_addr then
flag = 0;
end
end
//if scope not found, launch an error message and abort code execution
if flag == 1 then
disp("Scope not found, quitting script");
abort;
end
//check for connected signal generator
flag = 1;
for i = 1:nrow
if deviceAddr(i) == siggen_addr then
flag = 0;
end
end
//if siggen not found, launch an error message and abort code execution
if flag == 1 then
disp("Signal generator not found, quitting script");
abort;
end
disp("Instruments found");
//-------------------------CONNECT INSTRUMENTS----------------------------
[status,defaultRM] = viOpenDefaultRM();//open a session
[status,scopeID] = viOpen(defaultRM, scope_addr, viGetDefinition("VI_NULL"),viGetDefinition("VI_NULL"));//connect to scope
[status,siggenID] = viOpen(defaultRM, siggen_addr, viGetDefinition("VI_NULL"),viGetDefinition("VI_NULL"));//connect to scope
disp("Instruments connected");
//-------------------------SETUP INSTRUMENTS----------------------------
//----------setup scope "output" channel
//enable output channel
[status, count] = viWrite(scopeID,":CHANnel"+scope_out_ch+":DISPlay ON");
sleep(wait_set_time);
//set voltage range to default value
[status, count] = viWrite(scopeID,":CHANnel"+scope_out_ch+":SCALe "+string(vscale_opt(actual_vscale_out_opt,1)));
sleep(wait_set_time);
//----------setup scope "input" channel
//enable input channel
[status, count] = viWrite(scopeID,":CHANnel"+scope_in_ch+":DISPlay ON");
sleep(wait_set_time);
//set voltage range to default value
[status, count] = viWrite(scopeID,":CHANnel"+scope_in_ch+":SCALe "+string(vscale_opt(actual_vscale_in_opt,1)));
sleep(wait_set_time);
//----------disables unused channels
[status, count] = viWrite(scopeID,":CHANnel"+scope_unused_chA+":DISPlay OFF");
sleep(wait_set_time);
[status, count] = viWrite(scopeID,":CHANnel"+scope_unused_chB+":DISPlay OFF");
sleep(wait_set_time);
//----------set memory depth
[status, count] = viWrite(scopeID,":ACQuire:MDEPth "+string(mem_depth));
sleep(wait_set_time);
//---------- scope measure setup
//input Vpp
[status, count] = viWrite(scopeID,"MEASure:ITEM VPP,CHANnel"+scope_in_ch);
sleep(wait_set_time);
//output Vpp
[status, count] = viWrite(scopeID,"MEASure:ITEM VPP,CHANnel"+scope_out_ch);
sleep(wait_set_time);
//----------setup siggen
//set sinewave
[status, count] = viWrite(siggenID,siggen_out_ch+":BaSic_WaVe WVTP,SINE");
sleep(wait_set_time);
//set amplitude
[status, count] = viWrite(siggenID,siggen_out_ch+":BaSic_WaVe AMP,"+string(out_amplitude));
sleep(wait_set_time);
disp("Instruments ready");
//-------------------------START MEASURE----------------------------
//enable siggen out
[status, count] = viWrite(siggenID,siggen_out_ch+":OUTPut ON");
sleep(wait_set_time);
disp("Measure start, do not touch");
for i = 1:sample_n
[status, count] = viWrite(scopeID,":RUN");//start acquisition
sleep(wait_set_time);
try_counter = 1;//count how many times a measure has been attempted
//calculate appropriate horizontal scale
flag_time = 1;
for j = 1:hscale_opt_n(1,1)//check for all possible values
if flag_time == 1 then //if not found yet execute
if hscale_opt(j,1)*hscale_div > (n_of_period/f_vect(i,1)) then //if the scale is correct
flag_time = 0;
actual_hscale_opt = j;//save the option
end
end
if j == hscale_opt_n(1,1) && flag_time ==1 then
actual_hscale_opt = j;//if timebase overflow, set maximum timescale
end
end
//apply appropriate horizontal scale
[status, count] = viWrite(scopeID,":TIMebase:MAIN:SCALe "+string(hscale_opt(actual_hscale_opt,1)));
sleep(wait_set_time);
//calculate measurement delay time
wait_meas_time = hscale_opt(actual_hscale_opt,1)*hscale_div*1000*meas_safety_factor;//time in ms
//set output frequency
[status, count] = viWrite(siggenID,siggen_out_ch+":BaSic_WaVe FRQ,"+string(f_vect(i,1)));
sleep(wait_set_time);
//show measure status (not using mprintf)
if i == 1 then
disp("Step "+string(i)+" out of "+string(sample_n));
else
disp("");//to align removed lines
clc(2);//remove lines
disp("Step "+string(i)+" out of "+string(sample_n));
end
//measure and if it's not valid, repeat!
flag = 1;
while flag == 1
if try_counter > 1 then//restart the acquisition on retry
[status, count] = viWrite(scopeID,":RUN");//start acquisition
sleep(wait_set_time);
end
sleep(wait_meas_time);
[status, count] = viWrite(scopeID,":STOP");//stop acquisition
sleep(wait_set_time);
//---------- scope measures
//input Vpp
[status, count] = viWrite(scopeID,"MEASure:ITEM? VPP,CHANnel"+scope_in_ch);
[status, bufferOut, count] = viRead(scopeID, 255);
amplitude_in_temp = strtod(bufferOut);//save measured data
if amplitude_in_temp == NaN then//if amplitude is NaN -> saturating, assign full scale value
amplitude_in_temp = vscale_div*vscale_opt(actual_vscale_in_opt);
end
//output Vpp
[status, count] = viWrite(scopeID,"MEASure:ITEM? VPP,CHANnel"+scope_out_ch);
[status, bufferOut, count] = viRead(scopeID, 255);
amplitude_out_temp = strtod(bufferOut);//save measured data
if amplitude_out_temp == NaN then//if amplitude is NaN -> saturating, assign full scale value
amplitude_out_temp = vscale_div*vscale_opt(actual_vscale_out_opt);
end
//--------------check for dynamic ranges (in and out channels) flags
flag_in = 1;//used during in channel dynamic range check
flag_out = 1;//used during out channel dynamic range check
//--------------check for dynamic range on the output channel
if (amplitude_out_temp > vscale_opt(actual_vscale_out_opt,1)*vscale_div*v_up_th) || (amplitude_out_temp < vscale_opt(actual_vscale_out_opt,1)*vscale_div*v_down_th)then
//if acquired data is saturating or too small on the actual vertical scale, calculate new scale
flag_scale = 1
//check for repeated measure when saturating
if (actual_vscale_out_opt == vscale_opt_n(1,1)) && (amplitude_out_temp > vscale_opt(actual_vscale_out_opt,1)*vscale_div*v_up_th) then//I'm already at max scale and saturating?
//if so, don't repeat measure
flag_scale = 0;
flag_out = 0;
end
//I'm already at min scale and going down?
if (actual_vscale_out_opt == 1) && (amplitude_out_temp < vscale_opt(actual_vscale_out_opt,1)*vscale_div*v_down_th) then
//if so, don't repeat measure
flag_scale = 0;
flag_out = 0;
end
for j = 1:vscale_opt_n(1,1)//check for all possible values
if flag_scale == 1 then //if not found yet execute
if vscale_opt(j,1)*vscale_div*v_up_th > amplitude_out_temp then //if the scale is correct
flag_scale = 0;
actual_vscale_out_opt = j;//save the option
end
if j == vscale_opt_n(1,1) then //full scale?
actual_vscale_out_opt = j;//save the option, to avoid full scale values to not set max scale
end
end
end
//set output channel voltage range
[status, count] = viWrite(scopeID,":CHANnel"+scope_out_ch+":SCALe "+string(vscale_opt(actual_vscale_out_opt,1)));
sleep(wait_channel_vscale_setup);
try_counter = try_counter+1;//increase by one the counter
else
flag_out = 0;//no problem on this channel
end
//--------------check for dynamic range on the input channel
if (amplitude_in_temp > vscale_opt(actual_vscale_in_opt,1)*vscale_div*v_up_th) || (amplitude_in_temp < vscale_opt(actual_vscale_in_opt,1)*vscale_div*v_down_th)then
//if acquired data is saturating or too small on the actual vertical scale, calculate new scale
flag_scale = 1
//check for repeated measure when saturating
if (actual_vscale_in_opt == vscale_opt_n(1,1)) && (amplitude_in_temp > vscale_opt(actual_vscale_in_opt,1)*vscale_div*v_up_th) then//I'm already at max scale and saturating?
//if so, don't repeat measure
flag_scale = 0;
flag_in = 0;
end
//I'm already at min scale and going down?
if (actual_vscale_in_opt == 1) && (amplitude_in_temp < vscale_opt(actual_vscale_in_opt,1)*vscale_div*v_down_th) then
//if so, don't repeat measure
flag_scale = 0;
flag_in = 0;
end
for j = 1:vscale_opt_n(1,1)//check for all possible values
if flag_scale == 1 then //if not found yet execute
if vscale_opt(j,1)*vscale_div*v_up_th > amplitude_in_temp then //if the scale is correct
flag_scale = 0;
actual_vscale_in_opt = j;//save the option
end
if j == vscale_opt_n(1,1) then //full scale?
actual_vscale_in_opt = j;//save the option, to avoid full scale values to not set max scale
end
end
end
//set input channel voltage range
[status, count] = viWrite(scopeID,":CHANnel"+scope_in_ch+":SCALe "+string(vscale_opt(actual_vscale_in_opt,1)));
sleep(wait_channel_vscale_setup);
try_counter = try_counter+1;//increase by one the counter
else
flag_in = 0;//no problem on this channel
end
if (flag_in == 0) && (flag_out == 0) then//both channels are ok?
flag = 0;//if so, exit repeat loop for this measure
end
if try_counter > max_measure_attempts then//if reached max number of retry, go on (to avoid infinite loop)
flag = 0;
end
end//while repeated measure end
//--------------acquire waveforms
//input channel
[status, count] = viWrite(scopeID,"WAVeform:SOURce CHANnel"+scope_in_ch);//set input channel
[status, count] = viWrite(scopeID,"WAVeform:MODE RAW");//read waveform from internal memory
[status, count] = viWrite(scopeID,"WAVeform:FORMat ASCii");//data as double
[status, count] = viWrite(scopeID,"WAVeform:STARt 1");//read all points: from first to last
[status, count] = viWrite(scopeID,"WAVeform:STOP "+string(mem_depth));
[status, count] = viWrite(scopeID,"WAVeform:DATA?");//read the data
[status, bufferOut, count] = viRead(scopeID, mem_depth*14);//12+1(separator) ascii codes for each number + 1 as margin...
[ind, which] = strindex(bufferOut, ",");//find separators
ind = ind';//rotate vector, just to make my life easier
data_in = zeros(mem_depth-1,1);
//extract all data points, remove first because first number has 11 added chars that are constant... It's just easier to remove based on separator
for j = 2:mem_depth-1
minindex = ind(j-1,1)+1;
maxindex = ind(j,1)-1;
data_in(j-1,1) = strtod(part(bufferOut,[minindex:maxindex]));
end
//output channel
[status, count] = viWrite(scopeID,"WAVeform:SOURce CHANnel"+scope_out_ch);//set input channel
[status, count] = viWrite(scopeID,"WAVeform:MODE RAW");//read waveform from internal memory
[status, count] = viWrite(scopeID,"WAVeform:FORMat ASCii");//data as double
[status, count] = viWrite(scopeID,"WAVeform:STARt 1");//read all points: from first to last
[status, count] = viWrite(scopeID,"WAVeform:STOP "+string(mem_depth));
[status, count] = viWrite(scopeID,"WAVeform:DATA?");//read the data
[status, bufferOut, count] = viRead(scopeID, mem_depth*14);//12+1(separator) ascii codes for each number + 1 as margin...
[ind, which] = strindex(bufferOut, ",");//find separators
ind = ind';//rotate vector, just to make my life easier
data_out = zeros(mem_depth-1,1);
//extract all data points, remove first because first number has 11 added chars that are constant... It's just easier to remove based on separator
for j = 2:mem_depth-1
minindex = ind(j-1,1)+1;
maxindex = ind(j,1)-1;
data_out(j-1,1) = strtod(part(bufferOut,[minindex:maxindex]));
end
//--------------calculate fft, gain and phase
//calculate fft
fft_in = fft(data_in);
fft_out = fft(data_out);
//find theoretical index
center_index = ceil(hscale_div*hscale_opt(actual_hscale_opt)*f_vect(i,1));
//find magnitude peak around the center index, it's the same for both in and out
offset = 3;
[Amax_in iAmax] = max(abs(fft_in(center_index-offset:center_index+offset)));
iAmax = iAmax + center_index - 1 - offset;//the actual index, -1 because scilab index system starts from 1 and not from 0
//find out max
Amax_out = max(abs(fft_out(iAmax)));
//get phases
p_in = atan(imag(fft_in(iAmax)),real(fft_in(iAmax)));
p_out = atan(imag(fft_out(iAmax)),real(fft_out(iAmax)));
//calculate gain and phase
gain_vect(i,1) = Amax_out / Amax_in;
phase_vect(i,1) = (p_out - p_in)*180/%pi;//phase in degree
end//measurement loop end
//disable siggen out
[status, count] = viWrite(siggenID,siggen_out_ch+":OUTPut OFF");
sleep(wait_set_time);
disp("Measure completed");
//-----------------------CLOSE CONNECTIONS------------------------------------
viClose(scopeID);
viClose(siggenID);
viClose(defaultRM);
//----------------------------ELABORATE DATA------------------------------------
//--------------calculate gain (dB scale)
if y_type == "LOG" then
for i=1:sample_n
gain_vect(i,1) = 20*log10(gain_vect(i,1));
end
end
//--------------"wrap" phase into +- 180°
for i=1:sample_n
while phase_vect(i,1)>180 || phase_vect(i,1)<-180
if phase_vect(i,1)>180 then
phase_vect(i,1) = phase_vect(i,1)-360;
end
if phase_vect(i,1)<-180 then
phase_vect(i,1) = phase_vect(i,1)+360;
end
end
end
//--------------plot
f = figure(0);
f.background = 8;
//plot magnitude
subplot(2,1,1);
if sweep_type == "LIN" then
plot2d(f_vect,gain_vect);
else
plot2d("ln",f_vect,gain_vect);
end
if y_type == "LIN" then
ylabel("Magnitude []");
else
ylabel("Magnitude [dB]");
end
xlabel("Frequency [Hz]");
xgrid(1);
//plot phase
subplot(2,1,2);
if sweep_type == "LIN" then
plot2d(f_vect,phase_vect);
else
plot2d("ln",f_vect,phase_vect);
end
ylabel("Phase [°]");
xlabel("Frequency [Hz]");
xgrid(1);
Comparison: old vs new approach
Overall, I'm pleased with the outcome. It's easier to explain the improvement obtained with a couple of images.


Comparison measuring a RC filter


Comparison measuring loop stability of a buck converter
As it stands, this setup allows to get a complete acquisition with 40 points (two decades, 20 points per decade) in just under six minutes. It's not fast, but it's fast enough for all of my purposes.
Accelerated view during a sweep
The project within the project: fixing scope probes
During the script development, I spent quite a bit of time searching for the cause of inconsistent results and lack of repeatability. After some time, unable to find the corporate inside my code, I decided to check my scope probes, finding a surprisingly inconsinstent contact resistance when set to 1x (but it was also visible in 10x). The correct resistance for my probes (tip to BNC) is around 200Ω, but it was jumping around up to hundreds of kΩ at times.
Luckily, the probe tip/cable are intact and the problem was found in the hook tip, which was making bad contact with the rest of the probe. A cleanup with some contact cleaner somewhat improved the situation, but the final solution was mechanical: dismantling the hook tip and adding some insulation tape around the contact point to improve its stability.


Probe tip dismantled and the "repair"
It's kind of funny I was able to bring to conclusion with success this project thanks to a small piece of tape . And thanks to the e14 community that pushed me to start on again on this topic with this great challenge!

-
DAB
-
Cancel
-
Vote Up
0
Vote Down
-
-
Sign in to reply
-
More
-
Cancel
Comment-
DAB
-
Cancel
-
Vote Up
0
Vote Down
-
-
Sign in to reply
-
More
-
Cancel
Children