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!