diff --git a/@MyGuiCont/MyGuiCont.m b/@MyGuiCont/MyGuiCont.m index bc2cccb..88ae51a 100644 --- a/@MyGuiCont/MyGuiCont.m +++ b/@MyGuiCont/MyGuiCont.m @@ -1,35 +1,53 @@ % Class that provides basic functionality for handling App-based GUIs classdef MyGuiCont < handle properties (Access = public) % GUI object that is stored for reference only. Should include the % main figure. Gui % Name of the GUI class that can be used with the instrument gui_name char end methods (Access = public) + function this = MyGuiCont() + + % Create default name of the GUI based on the class name + class_name = class(this); + + % Optionally remove 'My' in front of the class name + tok = regexp(class_name, '(My)?(.+)','tokens'); + + try + if ~isempty(tok) + this.gui_name = ['Gui' tok{1}{2}]; + end + catch ME + warning(['Could not create default GUI name for ' ... + 'the class ' class_name '. Error:' ME.message]); + end + end + function createGui(this) assert(~isempty(this.gui_name), ['GUI name is not ' ... 'specified for the instrument class ' class(this)]); if isempty(this.Gui) || ~isvalid(this.Gui) this.Gui = feval(this.gui_name, this); end end end methods function set.Gui(this, Val) assert(~isempty(findFigure(Val)), ... 'Value assigned to Gui property must include a figure'); this.Gui = Val; end end end diff --git a/Instrument classes/@MyAgilentDso/MyAgilentDso.m b/Instrument classes/@MyAgilentDso/MyAgilentDso.m index 6df5bd9..3e10834 100644 --- a/Instrument classes/@MyAgilentDso/MyAgilentDso.m +++ b/Instrument classes/@MyAgilentDso/MyAgilentDso.m @@ -1,174 +1,184 @@ % Class for controlling 4-channel Agilent DSO scopes. % Tested with DSO7034A -classdef MyAgilentDso < MyScpiInstrument & MyDataSource & MyCommCont +classdef MyAgilentDso < MyScpiInstrument & MyDataSource & MyCommCont ... + & MyGuiCont properties (Constant = true) channel_no = 4 % number of channels end methods (Access = public) function this = MyAgilentDso(varargin) - this@MyCommCont(varargin{:}); + P = MyClassParser(this); + addParameter(P, 'enable_gui', false); + processInputs(P, this, varargin{:}); % 1.6e7 is the maximum trace size of DSO7034A %(8 mln point of 2-byte integers) this.Comm.InputBufferSize = 2e7; %byte this.Trace.name_x = 'Time'; this.Trace.name_y = 'Voltage'; this.Trace.unit_x = 's'; this.Trace.unit_y = 'V'; + connect(this); createCommandList(this); + + % There is high compatibility with Tektronix scope classes + this.gui_name = 'GuiTekScope'; + if P.Results.enable_gui + createGui(this); + end end function readTrace(this) this.Comm.ByteOrder = 'littleEndian'; % Set data format to be signed integer, reversed byte order, % 2 bytes per measurement point, read the maximum % available number of points writeStrings(this, ... ':WAVeform:BYTeorder LSBFirst', ... ':WAVeform:FORMat WORD', ... ':WAVeform:POINts:MODE MAX', ... ':WAVeform:UNSigned OFF', ... ':WAVeform:DATA?'); % Read the trace data y_data = int16(binblockread(this.Comm, 'int16')); % Read the preamble pre_str = queryString(this, ':WAVeform:PREamble?'); % Drop the end-of-the-string symbol and split pre = str2double(split(pre_str(1:end-1), ',')); step_x = pre(5); step_y = pre(8); x_zero = pre(6); y_zero = pre(9); % Calculate the y values y = double(y_data)*step_y + y_zero; n_points = length(y); % Calculate the x axis x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); this.Trace.x = x; this.Trace.y = y; triggerNewData(this); end function acquireContinuous(this) writeString(this, ':RUN'); end function acquireSingle(this) writeString(this, ':SINGLe'); end function stopAcquisition(this) writeString(this, ':STOP'); end % Emulates the physical knob turning, works with nturns=+-1 function turnKnob(this, knob, nturns) switch upper(knob) case 'HORZSCALE' % Timebase is changed if nturns == -1 this.time_scale = this.time_scale*2; elseif nturns == 1 this.time_scale = this.time_scale/2; else return end case {'VERTSCALE1', 'VERTSCALE2'} % Vertical scale is changed n_ch = sscanf(upper(knob), 'VERTSCALE%i'); tag = sprintf('scale%i', n_ch); if nturns==-1 this.(tag) = this.(tag)*2; elseif nturns==1 this.(tag) = this.(tag)/2; else return end end end end methods (Access = protected) function createCommandList(this) addCommand(this, 'channel', ':WAVeform:SOURce', ... 'format', 'CHAN%i', ... 'info', 'Channel from which the data is transferred'); addCommand(this, 'time_scale', ':TIMebase:SCALe',... 'format', '%e',... 'info', 'Time scale (s/div)'); addCommand(this, 'trig_lev', ':TRIGger:LEVel', ... 'format', '%e'); % trigger slope - works, but incompatible with Tektronix addCommand(this, 'trig_slope', ':TRIGger:SLOpe', ... 'format', '%s', ... 'value_list', {'NEGative', 'POSitive', 'EITHer', ... 'ALTernate'}); addCommand(this, 'trig_source', ':TRIGger:SOUrce', ... 'format', '%s', ... 'value_list', {'CHAN1', 'CHAN2', 'CHAN3', 'CHAN4',... 'EXT','LINE'}); % trigger mode addCommand(this, 'trig_mode', ':TRIGger:SWEep', ... 'format', '%s', ... 'value_list', {'AUTO', 'NORMal'}); addCommand(this, 'acq_mode', ':ACQuire:TYPE', ... 'format', '%s', ... 'info', ['Acquisition mode: normal(sample), ', ... 'high resolution or average'], ... 'value_list', {'NORMal', 'AVERage', 'HRESolution', ... 'PEAK'}); % Parametric commands for i = 1:this.channel_no i_str = num2str(i); addCommand(this, ... ['cpl' i_str], [':CHANnel' i_str ':COUPling'], ... 'format', '%s', ... 'info', 'Channel coupling: AC, DC or GND', ... 'value_list', {'AC','DC','GND'}); addCommand(this, ... ['imp' i_str], [':CHANnel' i_str ':IMPedance'], ... 'format', '%s', ... 'info', 'Channel impedance: 1 MOhm or 50 Ohm', ... 'value_list', {'FIFty','FIF','ONEMeg','ONEM'}); addCommand(this,... ['offset' i_str], [':CHANnel' i_str ':OFFSet'], ... 'format', '%e', ... 'info', '(V)'); addCommand(this,... ['scale' i_str], [':CHANnel' i_str ':SCAle'], ... 'format', '%e', ... 'info', 'Channel y scale (V/div)'); addCommand(this,... ['enable' i_str], [':CHANnel' i_str ':DISPlay'], ... 'format', '%b',... 'info', 'Channel enabled'); end end end end \ No newline at end of file diff --git a/Instrument classes/@MyAgilentNa/MyAgilentNa.m b/Instrument classes/@MyAgilentNa/MyAgilentNa.m index d5ff88a..3e40164 100644 --- a/Instrument classes/@MyAgilentNa/MyAgilentNa.m +++ b/Instrument classes/@MyAgilentNa/MyAgilentNa.m @@ -1,196 +1,195 @@ % The class for communication with Agilent E5061B Network Analyzer classdef MyAgilentNa < MyScpiInstrument & MyCommCont & MyDataSource ... & MyGuiCont properties(Access = public, SetObservable) Trace1 Trace2 transf_n = 1 % trace that triggers NewData event end properties (SetAccess = protected, GetAccess = public, SetObservable) % Manipulating active traces seems unavoidable for data format % selection. -1 stands for unknown. active_trace = -1 % data formats for the traces 1-2, options: % 'PHAS', 'SLIN', 'SLOG', 'SCOM', 'SMIT', 'SADM', 'MLOG', 'MLIN', %'PLIN', 'PLOG', 'POL' form1 = 'MLOG' form2 = 'PHAS' end methods function this = MyAgilentNa(varargin) P = MyClassParser(this); addParameter(P, 'enable_gui', false); processInputs(P, this, varargin{:}); this.Trace1 = MyTrace(); this.Trace2 = MyTrace(); this.Trace1.unit_x = 'Hz'; this.Trace1.name_x = 'Frequency'; this.Trace2.unit_x = 'Hz'; this.Trace2.name_x = 'Frequency'; connect(this); createCommandList(this); - this.gui_name = 'GuiAgilentNa'; if P.Results.enable_gui createGui(this); end end % Generate a new data event with header collection suppressed function transferTrace(this, n_trace) trace_tag = sprintf('Trace%i', n_trace); % Assign either Trace1 or 2 to Trace while keeping the metadata this.(trace_tag).UserMetadata = copy(this.Trace.UserMetadata); this.Trace = copy(this.(trace_tag)); triggerNewData(this, 'new_header', false); end function readTrace(this, n_trace) writeActiveTrace(this, n_trace); freq_str = strsplit(queryString(this,':SENS1:FREQ:DATA?'),','); data_str = strsplit(queryString(this,':CALC1:DATA:FDAT?'),','); data_x = str2double(freq_str); % In the returned string there is in general 2 values for each % frequency point. In the Smith data format this can be used to % transfer magnitude and phase of the signal in one trace. With % MLOG, MLIN and PHAS format settings every 2-nd element should % be 0 data_y1 = str2double(data_str(1:2:end)); % set the Trace properties trace_tag = sprintf('Trace%i', n_trace); this.(trace_tag).x = data_x; this.(trace_tag).y = data_y1; if this.transf_n == n_trace this.Trace = copy(this.(trace_tag)); triggerNewData(this); end end function writeActiveTrace(this, n_trace) writeString(this, sprintf(':CALC1:PAR%i:SEL', n_trace)); this.active_trace = n_trace; end function writeTraceFormat(this, n_trace, fmt) writeActiveTrace(this, n_trace); n_str = num2str(n_trace); this.(['form', n_str]) = fmt; writeString(this, sprintf(':CALC1:FORM %s', fmt)); end function singleSweep(this) % Set the triger source to remote control this.trig_source = 'BUS'; this.cont_trig = true; % Start a sweep cycle writeString(this, ':TRIG:SING'); % Wait for the sweep to finish (for the query to return 1) queryString(this, '*OPC?'); end function startContSweep(this) % Set the triger source to internal this.trig_source = 'INT'; this.cont_trig = true; end function abortSweep(this) this.trig_source = 'BUS'; writeString(this, ':ABOR'); end end methods (Access = protected) function createCommandList(this) addCommand(this, 'cent_freq', ':SENS1:FREQ:CENT', ... 'format', '%e', ... 'info', '(Hz)'); addCommand(this, 'start_freq', ':SENS1:FREQ:START', ... 'format', '%e',... 'info', '(Hz)'); addCommand(this, 'stop_freq', ':SENS1:FREQ:STOP', ... 'format', '%e',... 'info', '(Hz)'); addCommand(this, 'span', ':SENS1:FREQ:SPAN', ... 'format', '%e',... 'info', '(Hz)'); addCommand(this, 'ifbw', ':SENS1:BAND', ... 'format', '%e', ... 'info', 'IF bandwidth (Hz)'); addCommand(this, 'point_no', ':SENS1:SWE:POIN', ... 'format', '%i'); addCommand(this, 'average_no', ':SENS1:AVER:COUN', ... 'format', '%i'); addCommand(this, 'trace_no', ':CALC1:PAR:COUN', ... 'format', '%i',... 'info', 'Number of traces', ... 'value_list', {1, 2}); addCommand(this, 'sweep_type', ':SENS1:SWE:TYPE', ... 'format', '%s',... 'info', 'Linear or log sweep', ... 'value_list', {'LIN', 'LOG'}); addCommand(this, 'enable_out', ':OUTP', ... 'format', '%b',... 'info', 'output signal on/off'); addCommand(this, 'power', ':SOUR:POW:LEV:IMM:AMPL', ... 'format', '%e',... 'info', 'Probe power (dB)'); addCommand(this, 'disp_type', ':DISP:WIND1:SPL',... 'format', '%s',... 'info', 'Window arrangement', ... 'default', 'D1'); addCommand(this, 'cont_trig', ':INIT1:CONT', ... 'format', '%b'); addCommand(this, 'trig_source', ':TRIG:SOUR', ... 'format', '%s', ... 'default', 'BUS') % Parametric commands for traces, i can be extended to 4 for i = 1:2 % measurement parameters for the traces 1-2, e.g. 'S21' i_str = num2str(i); addCommand(this,... ['meas_par',i_str], [':CALC1:PAR',i_str,':DEF'], ... 'format', '%s',... 'info', 'Measurement parameter', ... 'default', 'S21'); end end end end diff --git a/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m b/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m index dcda232..abb7be4 100644 --- a/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m +++ b/Instrument classes/@MyColdEdgeCryo/MyColdEdgeCryo.m @@ -1,192 +1,199 @@ % Class for controlling the auto manifold of ColdEdge stinger cryostat. % The manifold is managed by an Arduino board that communicates with % computer via serial protocol. -classdef MyColdEdgeCryo < MyScpiInstrument & MyCommCont +classdef MyColdEdgeCryo < MyScpiInstrument & MyCommCont & MyGuiCont properties (GetAccess = public, SetAccess = protected, ... SetObservable = true) % If a long term operation (e.g. starting a cooldown or pumping the % capillary) is in progress operation_in_progress % Time for the recirculator to pump down helium from the capillary % before closing it off tr = 900 end properties (Access = protected) Timer end methods (Access = public) function this = MyColdEdgeCryo(varargin) - this@MyCommCont(varargin{:}); + P = MyClassParser(this); + addParameter(P, 'enable_gui', false); + processInputs(P, this, varargin{:}); this.Timer = timer(); % Buffer size of 64 kByte should be way an overkill. this.Device.InputBufferSize = 2^16; this.Device.OutputBufferSize = 2^16; + connect(this); createCommandList(this); + + if P.Results.enable_gui + createGui(this); + end end function delete(this) try stop(this.Timer); catch ME warning(ME.message); end try delete(this.Timer); catch ME warning(ME.message); end end % Abort the current operation function abort(this) stop(this.Timer); this.operation_in_progress = false; this.auto_sync = false; this.valve1 = false; this.valve2 = false; this.valve3 = false; this.valve4 = false; this.valve5 = false; this.valve7 = false; this.recirc = false; this.cryocooler = false; % Sync once sync(this); % Return to the hedged mode this.auto_sync = true; end function startCooldown(this) assert(~this.operation_in_progress, ['Cannot initiate' ... ' cooldown stop. Another operation is in progress.']) this.auto_sync = false; this.valve2 = false; this.valve3 = false; this.valve5 = false; this.valve7 = false; % Open the recirculator path, starting from the return this.valve4 = true; this.valve1 = true; % Start the compressors this.recirc = true; this.cryocooler = true; % Sync once sync(this); % Return to the hedged mode this.auto_sync = true; end function stopCooldown(this) function switchRecirculatorOff(~, ~) this.auto_sync = false; % Close the recirculator path, starting from the supply this.valve1 = false; this.valve4 = false; % Switch off the recirculator after all the valves are % closed this.recirc = false; sync(this); this.auto_sync = true; this.operation_in_progress = false; end assert(~this.operation_in_progress, ['Cannot initiate' ... ' cooldown stop. Another operation is in progress.']) this.auto_sync = false; % Switch off the cryocooler, close the recirculator supply % valve (1). this.valve1 = false; this.cryocooler = false; sync(this); this.auto_sync = true; % Wait for the helium to be pumped out of the capillary by the % recirculator and then switch the recirculator off this.Timer.ExecutionMode = 'singleShot'; this.Timer.StartDelay = this.tr; this.Timer.TimerFcn = @switchRecirculatorOff; start(this.Timer); this.operation_in_progress = true; end % Overload writeSettings method of MyInstrument function writeSettings(this) disp(['The settings of ' class(this) ' cannot be loaded ' ... 'for safety considerations. Please configure the ' ... 'instrument manually']) return end end methods (Access = protected) function createCommandList(this) % Valve states for i = 1:7 if i == 6 continue % There is no valve 6 end tag = sprintf('valve%i',i); cmd = sprintf(':VALVE%i',i); addCommand(this, tag, cmd, ... 'format', '%b', ... 'info', 'Valve open(true)/clsed(false)'); end addCommand(this, 'recirc', ':REC', ... 'format', '%b', ... 'info', 'Recirculator on/off'); addCommand(this, 'cryocooler', ':COOL', ... 'format', '%b', ... 'info', 'Cryocooler on/off'); addCommand(this, 'press', ':PRES', ... 'format', '%e', ... 'access', 'r', ... 'info', 'Supply pressure (PSI)'); end end methods function val = get.operation_in_progress(this) try val = strcmpi(this.Timer.Running, 'on'); catch ME warning(ME.message); val = false; end end end end diff --git a/Instrument classes/@MyTekRsa/MyTekRsa.m b/Instrument classes/@MyTekRsa/MyTekRsa.m index e5cd545..e1f0d82 100644 --- a/Instrument classes/@MyTekRsa/MyTekRsa.m +++ b/Instrument classes/@MyTekRsa/MyTekRsa.m @@ -1,200 +1,197 @@ % Class for controlling Tektronix RSA5103 and RSA5106 spectrum analyzers classdef MyTekRsa < MyScpiInstrument & MyDataSource & MyCommCont ... & MyGuiCont properties (SetAccess = protected, GetAccess = public) acq_trace % The number of last read trace end methods (Access = public) function this = MyTekRsa(varargin) P = MyClassParser(this); addParameter(P, 'enable_gui', false); processInputs(P, this, varargin{:}); this.Trace.unit_x = 'Hz'; this.Trace.unit_y = '$\mathrm{V}^2/\mathrm{Hz}$'; this.Trace.name_y = 'Power'; this.Trace.name_x = 'Frequency'; - - % Set default GUI name - this.gui_name = 'GuiTekRsa'; % Create communication object connect(this); % Set up the list of communication commands createCommandList(this); if P.Results.enable_gui createGui(this); end end end methods (Access = protected) function createCommandList(this) % We define commands for both the nominal and actual resolution % bandwidths as these two are useful in different % circumstances. The nominal one unlike the actual one takes % effect immediately after it is set to a new value, whereas % the actual one is the true rbw if the device does not follow % the nominal one (usually if the nominal rbw is is too small). addCommand(this, 'rbw', ':DPX:BANDwidth:RESolution', ... 'format', '%e', ... 'info', 'Nominal resolution bandwidth (Hz)'); addCommand(this, 'rbw_act', ':DPX:BANDwidth:ACTual', ... 'format', '%e', ... 'access', 'r', ... 'info', 'Actual resolution bandwidth (Hz)'); addCommand(this, 'auto_rbw', ':DPX:BAND:RES:AUTO', ... 'format', '%b'); addCommand(this, 'span', ':DPX:FREQ:SPAN', ... 'format', '%e', ... 'info', '(Hz)'); addCommand(this, 'start_freq', ':DPX:FREQ:STAR',... 'format', '%e', ... 'info', '(Hz)'); addCommand(this, 'stop_freq', ':DPX:FREQ:STOP',... 'format', '%e', ... 'info', '(Hz)'); addCommand(this, 'cent_freq', ':DPX:FREQ:CENT',... 'format', '%e', ... 'info', '(Hz)'); % Continuous triggering addCommand(this, 'init_cont', ':INIT:CONT', ... 'format', '%b',... 'info', 'Continuous triggering on/off'); % Number of points in trace addCommand(this, 'point_no', ':DPSA:POIN:COUN', ... 'format', 'P%i', ... 'value_list', {801, 2401, 4001, 10401}); % Reference level (dB) addCommand(this, 'ref_level',':INPut:RLEVel', ... 'format', '%e',... 'info', '(dB)'); % Display scale per division (dBm/div) addCommand(this, 'disp_y_scale', ':DISPlay:DPX:Y:PDIVision',... 'format', '%e', ... 'info', '(dBm/div)'); % Display vertical offset (dBm) addCommand(this, 'disp_y_offset', ':DISPLAY:DPX:Y:OFFSET', ... 'format', '%e', ... 'info', '(dBm)'); % Parametric commands for i = 1:3 i_str = num2str(i); % Display trace addCommand(this, ['disp_trace',i_str], ... [':TRAC',i_str,':DPX'], ... 'format', '%b', ... 'info', 'on/off'); % Trace Detection addCommand(this, ['det_trace',i_str],... [':TRAC',i_str,':DPX:DETection'],... 'format', '%s', ... 'value_list', {'AVERage', 'NEGative', 'POSitive'}); % Trace Function addCommand(this, ['func_trace',i_str], ... [':TRAC',i_str,':DPX:FUNCtion'], ... 'format', '%s', ... 'value_list', {'AVERage', 'HOLD', 'NORMal'}); % Number of averages addCommand(this, ['average_no',i_str], ... [':TRAC',i_str,':DPX:AVER:COUN'], ... 'format', '%i'); % Count completed averages addCommand(this, ['cnt_trace',i_str], ... [':TRACe',i_str,':DPX:COUNt:ENABle'], ... 'format', '%b', ... 'info', 'Count completed averages'); end end end methods (Access = public) function readTrace(this, varargin) if ~isempty(varargin) n_trace = varargin{1}; else n_trace = this.acq_trace; end % Ensure that device parameters, especially those that will be % later used for the calculation of frequency axis, are up to % date sync(this); writeString(this, sprintf('fetch:dpsa:res:trace%i?', n_trace)); data = binblockread(this.Comm, 'float'); % Calculate the frequency axis this.Trace.x = linspace(this.start_freq, this.stop_freq,... this.point_no); % Calculates the power spectrum from the data, which is in dBm. % Output is in V^2/Hz this.Trace.y = (10.^(data/10))/this.rbw_act*50*0.001; this.acq_trace = n_trace; % Trigger acquired data event triggerNewData(this); end % Abort data acquisition function abortAcq(this) writeString(this, ':ABORt'); end % Initiate data acquisition function initAcq(this) writeString(this, ':INIT'); end % Wait for the current operation to be completed function val = opc(this) val = queryString(this, '*OPC?'); end % Extend readSettings function function Hdr = readSettings(this) %Call parent class method and then append parameters Hdr = readSettings@MyScpiInstrument(this); %Hdr should contain single field addParam(Hdr, Hdr.field_names{1}, ... 'acq_trace', this.acq_trace, ... 'comment', 'The number of last read trace'); end end methods function set.acq_trace(this, val) assert((val==1 || val==2 || val==3), ... 'Acquisition trace number must be 1, 2 or 3.'); this.acq_trace = val; end end end diff --git a/Instrument classes/@MyTekScope/MyTekScope.m b/Instrument classes/@MyTekScope/MyTekScope.m index 0148064..1802189 100644 --- a/Instrument classes/@MyTekScope/MyTekScope.m +++ b/Instrument classes/@MyTekScope/MyTekScope.m @@ -1,112 +1,112 @@ % Generic class for controlling Tektronix scopes classdef MyTekScope < MyScpiInstrument & MyDataSource & MyCommCont ... & MyGuiCont properties (GetAccess = public, SetAccess={?MyClassParser,?MyTekScope}) % number of channels channel_no = 4 % List of the physical knobs, which can be rotated programmatically knob_list = {} end methods (Access = public) function this = MyTekScope(varargin) % Set default GUI name - this.gui_name = 'GuiTekRsa'; + this.gui_name = 'GuiTekScope'; this.Comm.InputBufferSize = 4.1e7; % byte this.Trace.name_x = 'Time'; this.Trace.name_y = 'Voltage'; end function readTrace(this) % Read raw y data y_data = readY(this); % Read units, offsets and steps for the scales parms = queryStrings(this, ... ':WFMOutpre:XUNit?', ... ':WFMOutpre:YUNit?', ... ':WFMOutpre:XINcr?', ... ':WFMOutpre:YMUlt?', ... ':WFMOutpre:XZEro?', ... ':WFMOutpre:YZEro?', ... ':WFMOutpre:YOFf?'); num_params = str2doubleHedged(parms); [unit_x, unit_y, step_x, step_y, x_zero, ... y_zero, y_offset] = num_params{:}; % Calculating the y data y = (y_data-y_offset)*step_y+y_zero; n_points = length(y); % Calculating the x data x = linspace(x_zero, x_zero + step_x*(n_points-1), n_points); this.Trace.x = x; this.Trace.y = y; % Discard "" when assiging the Trace labels this.Trace.unit_x = unit_x(2:end-1); this.Trace.unit_y = unit_y(2:end-1); triggerNewData(this); end function acquireContinuous(this) writeStrings(this, ... ':ACQuire:STOPAfter RUNSTop', ... ':ACQuire:STATE ON'); end function acquireSingle(this) writeStrings(this, ... ':ACQuire:STOPAfter SEQuence', ... ':ACQuire:STATE ON'); end function turnKnob(this, knob, nturns) writeString(this, sprintf(':FPAnel:TURN %s,%i', knob, nturns)); end end methods (Access = protected) % The default version of this method works for DPO3034-4034 scopes function y_data = readY(this) % Configure data transfer: binary format and two bytes per % point. Then query the trace. this.Comm.ByteOrder = 'bigEndian'; writeStrings(this, ... ':DATA:ENCDG RIBinary', ... ':DATA:WIDTH 2', ... ':DATA:STARt 1', ... sprintf(':DATA:STOP %i', this.point_no), ... ':CURVE?'); y_data = double(binblockread(this.Comm, 'int16')); % For some reason MDO3000 scope needs to have an explicit pause % between data reading and any other communication pause(0.01); end end methods function set.knob_list(this, val) assert(iscellstr(val), ['Value must be a cell array of ' ... 'character strings.']) %#ok this.knob_list = val; end end end diff --git a/Instrument classes/@MyZiRingdown/MyZiRingdown.m b/Instrument classes/@MyZiRingdown/MyZiRingdown.m index dd62217..7c29e63 100644 --- a/Instrument classes/@MyZiRingdown/MyZiRingdown.m +++ b/Instrument classes/@MyZiRingdown/MyZiRingdown.m @@ -1,935 +1,934 @@ % Class for performing ringdown measurements of mechanical oscillators % using Zurich Instruments UHF or MF lock-in. % % Operation: sweep the driving tone (drive_osc) using the sweep module % in LabOne web user interface, when the magnitude of the demodulator % signal exceeds trig_threshold the driving tone is switched off and % the recording of demodulated signal is started, the signal is recorded % for the duration of record_time. % % Features: % % Adaptive measurement oscillator frequency % % Averaging % % Auto saving % % Auxiliary output signal: If enable_aux_out=true % then after a ringdown is started a sequence of pulses is applied % to the output consisting of intermittent on and off periods % starting from on. classdef MyZiRingdown < MyZiLockIn & MyDataSource & MyGuiCont properties (Access = public, SetObservable = true) % Ringdown is recorded if the signal in the triggering demodulation % channel exceeds this value trig_threshold = 1e-3 % V % Duration of the recorded ringdown record_time = 1 % (s) % If enable_acq is true, then the drive is on and the acquisition % of record is triggered when signal exceeds trig_threshold enable_acq = false % Auxiliary output signal during ringdown. enable_aux_out = false % If auxiliary output is applied % time during which the output is in aux_out_on_lev state aux_out_on_t = 1 % (s) % time during which the output is in aux_out_off_lev state aux_out_off_t = 1 % (s) aux_out_on_lev = 1 % (V), output trigger on level aux_out_off_lev = 0 % (V), output trigger off level % Average the trace over n points to reduce amount of stored data % while keeping the demodulator bandwidth large downsample_n = 1 fft_length = 128 auto_save = false % if all ringdowns should be automatically saved % In adaptive measurement oscillator mode the oscillator frequency % is continuously changed to follow the signal frequency during % ringdown acquisition. This helps against the oscillator frequency % drift. adaptive_meas_osc = false end % The properties which are read or set only once during the class % initialization properties (GetAccess = public, SetAccess = {?MyClassParser}, ... SetObservable = true) % enumeration for demodulators, oscillators and output starts from 1 demod = 1 % demodulator used for both triggering and measurement % Enumeration in the node structure starts from 0, so, for example, % the default path to the trigger demodulator refers to the % demodulator #1 demod_path = '/dev4090/demods/0' drive_osc = 1 meas_osc = 2 % Signal input, integers above 1 correspond to main inputs, aux % input etc. (see the user interface for device-specific details) signal_in = 1 drive_out = 1 % signal output used for driving % Number of an auxiliary channel used for the output of triggering % signal, primarily intended to switch the measurement apparatus % off during a part of the ringdown and thus allow for free % evolution of the oscillator during that period. aux_out = 1 % Poll duration of 1 ms practically means that ziDAQ('poll', ... % returns immediately with the data accumulated since the % previous function call. poll_duration = 0.001 % s poll_timeout = 50 % ms % Margin for adaptive oscillator frequency adjustment - oscillator % follows the signal if the dispersion of frequency in the % demodulator band is below ad_osc_margin times the demodulation % bandwidth (under the condition that adaptive_meas_osc=true) ad_osc_margin = 0.1 end % Internal variables properties (GetAccess = public, SetAccess = protected, ... SetObservable = true) recording = false % true if a ringdown is being recorded % true if adaptive measurement oscillator mode is on and if the % measurement oscillator is actually actively following. ad_osc_following = false % Reference timestamp at the beginning of measurement record. % Stored as uint64. t0 elapsed_t = 0 % Time elapsed since the last recording was started DemodSpectrum % MyTrace object to store FFT of the demodulator data end % Other dependent variables that are not device properties properties (Dependent = true) % Downsample the measurement record to reduce the amount of data % while keeping the large demodulation bandwidth. % (samples/s), sampling rate of the trace after avraging downsampled_rate % Provides public access to properties of private AvgTrace n_avg % number of ringdowns to be averaged avg_count % the average counter fft_rbw % resolution bandwidth of fft poll_period % (s) end % Keeping handle objects fully private is the only way to restrict set % access to their properties properties (Access = private) PollTimer AuxOutOffTimer % Timer responsible for switching the aux out off AuxOutOnTimer % Timer responsible for switching the aux out on % Demodulator samples z(t) stored to continuously calculate % spectrum, the values of z are complex here, z=x+iy. % osc_freq is the demodulation frequency DemodRecord = struct('t',[],'z',[],'osc_freq',[]) AvgTrace % MyAvgTrace object used for averaging ringdowns end events NewDemodSample % New demodulator samples received RecordingStarted % Acquisition of a new trace triggered end methods (Access = public) %% Constructor and destructor function this = MyZiRingdown(varargin) P = MyClassParser(this); addParameter(P, 'poll_period', 0.1, @isnumeric); addParameter(P, 'enable_gui', false); processInputs(P, this, varargin{:}); % Create and configure trace objects % Trace is inherited from the superclass this.Trace = MyTrace(... 'name_x','Time',... 'unit_x','s',... 'name_y','Magnitude r',... 'unit_y','V'); this.DemodSpectrum = MyTrace(... 'name_x','Frequency',... 'unit_x','Hz',... 'name_y','PSD',... 'unit_y','V^2/Hz'); this.AvgTrace = MyAvgTrace(); % Set up the poll timer. Using a timer for anyncronous % data readout allows to use the wait time for execution % of other programs. % Fixed spacing is preferred as it is the most robust mode of % operation when keeping the intervals between callbacks % precisely defined is not the biggest concern. % Busy mode is 'drop' - there is no need to accumulate timer % callbacks as the data is stored in the buffer of zi data % server since the previous poll. this.PollTimer = timer(... 'BusyMode', 'drop',... 'ExecutionMode', 'fixedSpacing',... 'Period', P.Results.poll_period,... 'TimerFcn', @this.pollTimerCallback); % Aux out timers use fixedRate mode for more precise timing. % The two timers are executed periodically with a time lag. % The first timer switches the auxiliary output off this.AuxOutOffTimer = timer(... 'ExecutionMode', 'fixedRate',... 'TimerFcn', @this.auxOutOffTimerCallback); % The second timer switches the auxiliary output on this.AuxOutOnTimer = timer(... 'ExecutionMode', 'fixedRate',... 'TimerFcn', @this.auxOutOnTimerCallback); this.demod_path = sprintf('/%s/demods/%i', this.dev_id, ... this.demod-1); createApiSession(this); createCommandList(this); - this.gui_name = 'GuiZiRingdown'; if P.Results.enable_gui createGui(this); end end function delete(this) % delete function should never throw errors, so protect % statements with try-catch try stopPoll(this) catch warning(['Could not usubscribe from the demodulator ', ... 'or stop the poll timer.']) end % Delete timers to prevent them from running indefinitely in % the case of program crash try delete(this.PollTimer) catch warning('Could not delete the poll timer.') end try stop(this.AuxOutOffTimer); delete(this.AuxOutOffTimer); catch warning('Could not stop and delete AuxOutOff timer.') end try stop(this.AuxOutOnTimer); delete(this.AuxOutOnTimer); catch warning('Could not stop and delete AuxOutOn timer.') end end %% Other methods function startPoll(this) sync(this); % Configure the oscillators, demodulator and driving output % -1 accounts for the difference in enumeration conventions % in the software names (starting from 1) and node numbers % (starting from 0). % First, update the demodulator path this.demod_path = sprintf('/%s/demods/%i', ... this.dev_id, this.demod-1); % Set the data transfer rate so that it satisfies the Nyquist % criterion (>x2 the bandwidth of interest) this.demod_rate = 4*this.lowpass_bw; % Configure the demodulator. Signal input: ziDAQ('setInt', ... [this.demod_path,'/adcselect'], this.signal_in-1); % Oscillator: ziDAQ('setInt', ... [this.demod_path,'/oscselect'], this.drive_osc-1); % Enable data transfer from the demodulator to the computer ziDAQ('setInt', [this.demod_path,'/enable'], 1); % Configure the signal output - disable all the oscillator % contributions excluding the driving tone path = sprintf('/%s/sigouts/%i/enables/*', ... this.dev_id, this.drive_out-1); ziDAQ('setInt', path, 0); path = sprintf('/%s/sigouts/%i/enables/%i', ... this.dev_id, this.drive_out-1, this.drive_osc-1); ziDAQ('setInt', path, 1); % By convention, we start form 'enable_acq=false' state this.enable_acq = false; this.drive_on = false; % Configure the auxiliary trigger output - put it in the manual % mode so it does not output demodulator readings path = sprintf('/%s/auxouts/%i/outputselect', ... this.dev_id, this.aux_out-1); ziDAQ('setInt', path, -1); % The convention is that aux out is on by default this.aux_out_on = true; % Subscribe to continuously receive samples from the % demodulator. Samples accumulated between timer callbacks % will be read out using ziDAQ('poll', ... ziDAQ('subscribe', [this.demod_path,'/sample']); % Start continuous polling start(this.PollTimer) end function stopPoll(this) stop(this.PollTimer) ziDAQ('unsubscribe', [this.demod_path,'/sample']); end % Main function that polls data from the device demodulator function pollTimerCallback(this, ~, ~) % Switch off the hedged mode to reduce latency this.auto_sync = false; % ziDAQ('poll', ... with short poll_duration returns % immediately with the data accumulated since the last timer % callback Data = ziDAQ('poll', this.poll_duration, this.poll_timeout); try % Get the new demodulator data DemodSample = Data.(this.dev_id).demods(this.demod).sample; catch this.auto_sync = true; return end % Append new samples to the record and recalculate spectrum appendSamplesToBuff(this, DemodSample); calcfft(this); if this.recording % If the recording has just started, save the start time if isempty(this.Trace.x) this.t0 = DemodSample.timestamp(1); end % If recording is under way, append the new samples to % the trace rec_finished = appendSamplesToTrace(this, DemodSample); % Update elapsed time if ~isempty(this.Trace.x) this.elapsed_t = this.Trace.x(end); else this.elapsed_t = 0; end % If the adaptive measurement frequency mode is on, % update the measurement oscillator frequency. % Make sure that the demodulator record actually % contains a signal by comparing the dispersion of % frequency to the demodulator bandwidth. if this.adaptive_meas_osc [df_avg, df_dev] = calcfreq(this); if df_dev < this.ad_osc_margin*this.lowpass_bw this.meas_osc_freq = df_avg; % Change indicator this.ad_osc_following = true; else this.ad_osc_following = false; end else this.ad_osc_following = false; end else r = sqrt(DemodSample.x.^2+DemodSample.y.^2); if this.enable_acq && max(r)>this.trig_threshold % Start acquisition of a new trace if the maximum % of the signal exceeds threshold this.recording = true; this.elapsed_t = 0; % Switch the drive off this.drive_on = false; % Set the measurement oscillator frequency to be % the frequency at which triggering occurred this.meas_osc_freq = this.drive_osc_freq; % Switch the oscillator this.current_osc = this.meas_osc; % Clear the buffer on ZI data server from existing % demodulator samples, as these samples were % recorded with drive on ziDAQ('poll', this.poll_duration, this.poll_timeout); % Optionally start the auxiliary output timers if this.enable_aux_out % Configure measurement periods and delays T = this.aux_out_on_t + this.aux_out_off_t; this.AuxOutOffTimer.Period = T; this.AuxOutOnTimer.Period = T; this.AuxOutOffTimer.startDelay =... this.aux_out_on_t; this.AuxOutOnTimer.startDelay = T; % Start timers start(this.AuxOutOffTimer) start(this.AuxOutOnTimer) end % Clear trace clearData(this.Trace); notify(this, 'RecordingStarted'); end rec_finished = false; % Indicator for adaptive measurement is off, since % recording is not under way this.ad_osc_following = false; end notify(this,'NewDemodSample'); % Stop recording if a ringdown record was completed if rec_finished % stop recording this.recording = false; this.ad_osc_following = false; % Stop auxiliary timers stop(this.AuxOutOffTimer); stop(this.AuxOutOnTimer); % Return the drive and aux out to the default state this.aux_out_on = true; this.current_osc = this.drive_osc; % Do trace averaging. If the new data length is not of % the same size as the length of the existing data % (which should happen only when the record period was % changed during recording or when recording was % manually stopped), truncate to the minimum length if ~isempty(this.AvgTrace.x) && ... (length(this.AvgTrace.y)~=length(this.Trace.y)) l = min(length(this.AvgTrace.y), ... length(this.Trace.y)); this.AvgTrace.y = this.AvgTrace.y(1:l); this.AvgTrace.x = this.AvgTrace.x(1:l); this.Trace.y = this.Trace.y(1:l); this.Trace.x = this.Trace.x(1:l); disp('Ringdown record was truncated') end avg_compl = addAverage(this.AvgTrace, this.Trace); % Trigger NewData if this.n_avg>1 end_str = sprintf('_%i', this.AvgTrace.avg_count); else end_str = ''; end triggerNewData(this, 'save', this.auto_save, ... 'filename_ending', end_str); % If the ringdown averaging is complete, disable % further triggering to exclude data overwriting if avg_compl this.enable_acq = false; this.drive_on = false; if this.n_avg>1 end_str = '_avg'; % Trigger one more time to transfer the average % trace. % A new measurement header is not necessary % as the delay since the last triggering is % minimum. triggerNewData(this, ... 'Trace', copy(this.AvgTrace), ... 'save', this.auto_save, ... 'filename_ending', end_str); end else % Continue trying to acquire new ringdowns this.enable_acq = true; this.drive_on = true; end end this.auto_sync = true; end % Append timestamps vs r=sqrt(x^2+y^2) to the measurement record. % Starting index can be supplied as varargin. % The output variable tells if the record is finished. function isfin = appendSamplesToTrace(this, DemodSample) persistent ts_buff r_sq_buff r_sq = DemodSample.x.^2 + DemodSample.y.^2; % Subtract the reference time, convert timestamps to seconds ts = double(DemodSample.timestamp - this.t0)/this.clockbase; % Check if recording should be stopped isfin = (ts(end) >= this.record_time); if isfin % Remove excess data points from the new data ind = (tsflen this.DemodRecord.t = this.DemodRecord.t(end-flen+1:end); this.DemodRecord.z = this.DemodRecord.z(end-flen+1:end); this.DemodRecord.osc_freq = ... this.DemodRecord.osc_freq(end-flen+1:end); end end function calcfft(this) flen = min(this.fft_length, length(this.DemodRecord.t)); [freq, spectr] = xyFourier( ... this.DemodRecord.t(end-flen+1:end), ... this.DemodRecord.z(end-flen+1:end)); this.DemodSpectrum.x = freq; this.DemodSpectrum.y = abs(spectr).^2; end % Calculate the average frequency and dispersion of the demodulator % signal function [f_avg, f_dev] = calcfreq(this) if ~isempty(this.DemodSpectrum.x) norm = sum(this.DemodSpectrum.y); % Calculate the center frequency of the spectrum f_avg = dot(this.DemodSpectrum.x, ... this.DemodSpectrum.y)/norm; f_dev = sqrt(dot(this.DemodSpectrum.x.^2, ... this.DemodSpectrum.y)/norm-f_avg^2); % Shift the FFT center by the demodulation frequency to % output absolute value f_avg = f_avg + mean(this.DemodRecord.osc_freq); else f_avg = []; f_dev = []; end end % Provide restricted access to private AvgTrace function resetAveraging(this) % Clear data and reset the counter clearData(this.AvgTrace); end function auxOutOffTimerCallback(this, ~, ~) this.aux_out_on = false; end function auxOutOnTimerCallback(this, ~, ~) this.aux_out_on = true; end end methods (Access = protected) function createCommandList(this) addCommand(this, 'drive_osc_freq', ... 'readFcn', @this.readDriveOscFreq, ... 'writeFcn', @this.writeDriveOscFreq, ... 'info', '(Hz)'); addCommand(this, 'meas_osc_freq', ... 'readFcn', @this.readMeasOscFreq, ... 'writeFcn', @this.writeMeasOscFreq, ... 'info', '(Hz)'); addCommand(this, 'drive_on', ... 'readFcn', @this.readDriveOn, ... 'writeFcn', @this.writeDriveOn); addCommand(this, 'current_osc', ... 'readFcn', @this.readCurrentOsc, ... 'writeFcn', @this.writeCurrentOsc, ... 'info', 'measurement or driving'); addCommand(this, 'drive_amp', ... 'readFcn', @this.readDriveAmp, ... 'writeFcn', @this.writeDriveAmp, ... 'info', '(Vpk)'); addCommand(this, 'lowpass_order', ... 'readFcn', @this.readLowpassOrder, ... 'writeFcn', @this.writeLowpassOrder, ... 'default', 1); addCommand(this, 'lowpass_bw', ... 'readFcn', @this.readLowpassBw, ... 'writeFcn', @this.writeLowpassBw, ... 'info', '3 db bandwidth of lowpass filter (Hz)'); addCommand(this, 'demod_rate', ... 'readFcn', @this.readDemodRate, ... 'writeFcn', @this.writeDemodRate, ... 'info', ['Rate at which demodulator data is ' ... 'transferred to computer']); addCommand(this, 'aux_out_on', ... 'readFcn', @this.readAuxOutOn, ... 'writeFcn', @this.writeAuxOutOn, ... 'info', 'If aux out in ''on'' state, true/false'); end function val = readDriveOscFreq(this) path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... this.drive_osc-1); val = ziDAQ('getDouble', path); end function writeDriveOscFreq(this, val) path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... this.drive_osc-1); ziDAQ('setDouble', path, val); end function val = readMeasOscFreq(this) path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... this.meas_osc-1); val = ziDAQ('getDouble', path); end function writeMeasOscFreq(this, val) path = sprintf('/%s/oscs/%i/freq', this.dev_id, ... this.meas_osc-1); ziDAQ('setDouble', path, val); end function val = readDriveOn(this) path = sprintf('/%s/sigouts/%i/on', this.dev_id, ... this.drive_out-1); val = logical(ziDAQ('getInt', path)); end function writeDriveOn(this, val) path = sprintf('/%s/sigouts/%i/on', this.dev_id, ... this.drive_out-1); % Use double() to convert from logical ziDAQ('setInt', path, double(val)); end function val = readCurrentOsc(this) val = double(ziDAQ('getInt', ... [this.demod_path,'/oscselect']))+1; end function writeCurrentOsc(this, val) assert((val==this.drive_osc) || (val==this.meas_osc), ... ['The number of current oscillator must be that of ', ... 'the drive or measurement oscillator, not ', num2str(val)]) ziDAQ('setInt', [this.demod_path,'/oscselect'], val-1); end function val = readDriveAmp(this) path = sprintf('/%s/sigouts/%i/amplitudes/%i', ... this.dev_id, this.drive_out-1, this.drive_osc-1); val = ziDAQ('getDouble', path); end function writeDriveAmp(this, val) path=sprintf('/%s/sigouts/%i/amplitudes/%i', ... this.dev_id, this.drive_out-1, this.drive_osc-1); ziDAQ('setDouble', path, val); end function n = readLowpassOrder(this) n = ziDAQ('getInt', [this.demod_path,'/order']); end function writeLowpassOrder(this, val) assert(any(val==[1,2,3,4,5,6,7,8]), ['Low-pass filter ', ... 'order must be an integer between 1 and 8']) ziDAQ('setInt', [this.demod_path,'/order'], val); end function bw = readLowpassBw(this) tc = ziDAQ('getDouble', [this.demod_path,'/timeconstant']); bw = ziTC2BW(tc, this.lowpass_order); end function writeLowpassBw(this, val) tc = ziBW2TC(val, this.lowpass_order); ziDAQ('setDouble', [this.demod_path,'/timeconstant'], tc); end function val = readDemodRate(this) val = ziDAQ('getDouble', [this.demod_path,'/rate']); end function writeDemodRate(this, val) ziDAQ('setDouble', [this.demod_path,'/rate'], val); end function bool = readAuxOutOn(this) path = sprintf('/%s/auxouts/%i/offset', ... this.dev_id, this.aux_out-1); val = ziDAQ('getDouble', path); % Signal from the auxiliary output is continuous, we make the % binary decision about the output state depending on if % the signal is closer to the ON or OFF level bool = (abs(val-this.aux_out_on_lev) < ... abs(val-this.aux_out_off_lev)); end function writeAuxOutOn(this, bool) path = sprintf('/%s/auxouts/%i/offset', ... this.dev_id, this.aux_out-1); if bool out_offset = this.aux_out_on_lev; else out_offset = this.aux_out_off_lev; end ziDAQ('setDouble', path, out_offset); end function createMetadata(this) createMetadata@MyZiLockIn(this); % Demodulator parameters addObjProp(this.Metadata, this, 'demod', 'comment', ... 'Number of the demodulator in use (starting from 1)'); addObjProp(this.Metadata, this, 'meas_osc', 'comment', ... 'Measurement oscillator number'); % Signal input addObjProp(this.Metadata, this, 'signal_in', 'comment', ... 'Singnal input number'); % Drive parameters addObjProp(this.Metadata, this, 'drive_out', 'comment', ... 'Driving output number'); addObjProp(this.Metadata, this, 'drive_osc', 'comment', ... 'Swept oscillator number'); % Parameters of the auxiliary output addObjProp(this.Metadata, this, 'aux_out', 'comment', ... 'Auxiliary output number'); addObjProp(this.Metadata, this, 'enable_aux_out', 'comment',... 'Auxiliary output is applied during ringdown'); addObjProp(this.Metadata, this, 'aux_out_on_lev', ... 'comment', '(V)'); addObjProp(this.Metadata, this, 'aux_out_off_lev', ... 'comment', '(V)'); addObjProp(this.Metadata, this, 'aux_out_on_t', ... 'comment', '(s)'); addObjProp(this.Metadata, this, 'aux_out_off_t', ... 'comment', '(s)'); % Software parameters addObjProp(this.Metadata, this, 'trig_threshold', 'comment',... '(V), threshold for starting a ringdown record'); addObjProp(this.Metadata, this, 'record_time', ... 'comment', '(s)'); addObjProp(this.Metadata, this, 'downsampled_rate', ... 'comment', ['(samples/s), rate to which a ringown ', ... 'trace is downsampled with averaging after acquisition']); addObjProp(this.Metadata, this, 'auto_save', 'comment', '(s)'); % Adaptive measurement oscillator addObjProp(this.Metadata, this, 'adaptive_meas_osc', ... 'comment', ['If true the measurement oscillator ', ... 'frequency is adjusted during ringdown']); addObjProp(this.Metadata, this, 'ad_osc_margin'); addObjProp(this.Metadata, this, 'fft_length', ... 'comment', '(points)'); % Timer poll parameters addParam(this.Metadata, 'poll_period', [],... 'comment', '(s)'); addObjProp(this.Metadata, this, 'poll_duration', ... 'comment', '(s)'); addObjProp(this.Metadata, this, 'poll_timeout', ... 'comment', '(ms)'); end end %% Set and get methods. methods function set.downsample_n(this, val) n = round(val); assert(n>=1, ['Number of points for trace averaging must ', ... 'be greater than 1']) this.downsample_n = n; end function set.downsampled_rate(this, val) dr = this.demod_rate; % Downsampled rate should not exceed the data transfer rate val = min(val, dr); % Round so that the averaging is done over an integer number of % points this.downsample_n = round(dr/val); end function val = get.downsampled_rate(this) val = this.demod_rate/this.downsample_n; end function set.fft_length(this, val) % Round val to the nearest 2^n to make the calculation of % Fourier transform efficient n = round(log2(max(val, 1))); this.fft_length = 2^n; end function val = get.fft_rbw(this) val = this.demod_rate/this.fft_length; end function set.fft_rbw(this, val) assert(val>0,'FFT resolution bandwidth must be greater than 0') % Rounding of fft_length to the nearest integer is handled by % its own set method this.fft_length = this.demod_rate/val; end function set.n_avg(this, val) this.AvgTrace.n_avg = val; end function val = get.n_avg(this) val = this.AvgTrace.n_avg; end function val = get.avg_count(this) val = this.AvgTrace.avg_count; end function set.aux_out_on_t(this, val) assert(val>0.001, ... 'Aux out on time must be greater than 0.001 s.') this.aux_out_on_t = val; end function set.aux_out_off_t(this, val) assert(val>0.001, ... 'Aux out off time must be greater than 0.001 s.') this.aux_out_off_t = val; end function set.enable_acq(this, val) this.enable_acq = logical(val); end function val = get.poll_period(this) val = this.PollTimer.Period; end end end diff --git a/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m b/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m index 53e5d28..81e621e 100644 --- a/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m +++ b/Instrument classes/@MyZiScopeFt/MyZiScopeFt.m @@ -1,252 +1,251 @@ % Spectrum analyzer based on Zurich Instruments UHFLI or MFLI classdef MyZiScopeFt < MyZiLockIn & MyDataSource & MyGuiCont properties (GetAccess = public, SetAccess = {?MyClassParser}) n_scope = 1 % number of hardware scope n_ch = 1 % number of scope channel % Input numbers between 1 and 148 correspond to various signals % including physical inputs, outputs, demodulator channels and % the results of arthmetic operations. See the LabOne user % interface for the complete list of choices and corresponding % numbers. This number is shifted by +1 compare to the hardware % node enumeration as usual. signal_in = 1 % Deas time between scope frame acquisitions. Smaller time results % in faster averaging but may not look nice during real time % gui update. trigholdoff = 0.02 % seconds end properties (Access = private) scope_module % 'handle' (in quotes) of a ZI software scope module PollTimer % Timer that regularly reads data drom the scope TmpTrace % Temporary variable used for averaging end properties (Dependent = true) scope_path fft_rbw % Spacing between fft bins poll_period end events NewWave % Triggered when the scope acquires new waves end methods (Access = public) function this = MyZiScopeFt(varargin) P = MyClassParser(this); addParameter(P, 'poll_period', 0.1, @isnumeric); addParameter(P, 'enable_gui', false); processInputs(P, this, varargin{:}); % Trace object in this case is directly used for averaging this.Trace = MyAvgTrace(... 'name_x','Time',... 'unit_x','s',... 'name_y','Magnitude r',... 'unit_y','V'); this.TmpTrace = MyTrace(); this.PollTimer = timer(... 'ExecutionMode', 'fixedSpacing',... 'Period', P.Results.poll_period,... 'TimerFcn', @(~,~)pollTimerCallback(this)); createApiSession(this); createCommandList(this); - this.gui_name = 'GuiZiScopeFt'; if P.Results.enable_gui createGui(this); end end function delete(this) % delete function should never throw errors, so protect % statements with try-catch try stopPoll(this) catch warning(['Could not usubscribe from the scope node ', ... 'or stop the poll timer.']) end % Clear the module's thread. try ziDAQ('clear', this.scope_module); catch warning('Could not clear the scope module.') end % Delete timers to prevent them from running indefinitely in % the case of program crash try delete(this.PollTimer) catch warning('Could not delete the poll timer.') end end function startPoll(this) % Configure hardware scope % Signal input path = sprintf('%s/channels/%i/inputselect', ... this.scope_path, this.n_ch); ziDAQ('setInt', path, this.signal_in-1); % Disable segmented mode of data transfer. This mode is only % useful if records longer than 5Mpts are required. ziDAQ('setInt', [this.scope_path '/segments/enable'], 0); % Take continuous records ziDAQ('setInt', [this.scope_path '/single'], 0); % Disable the scope trigger ziDAQ('setInt', [this.scope_path '/trigenable'], 0); % The scope hold off time inbetween acquiring triggers (still % relevant if triggering is disabled). ziDAQ('setDouble', [this.scope_path '/trigholdoff'], ... this.trigholdoff); % Enable the scope ziDAQ('setInt', [this.scope_path '/enable'], 1); % Initialize and configure a software Scope Module. this.scope_module = ziDAQ('scopeModule'); % Do not average ziDAQ('set', this.scope_module, ... 'scopeModule/averager/weight', 1); % Set the Scope Module's mode to return frequency domain data. ziDAQ('set', this.scope_module, 'scopeModule/mode', 3); % Use rectangular window function. ziDAQ('set', this.scope_module, 'scopeModule/fft/window', 0); ziDAQ('set', this.scope_module, 'scopeModule/fft/power', 1); ziDAQ('set', this.scope_module, ... 'scopeModule/fft/spectraldensity', 1); ziDAQ('subscribe', this.scope_module, ... [this.scope_path '/wave']); ziDAQ('execute', this.scope_module); start(this.PollTimer); end function stopPoll(this) stop(this.PollTimer); ziDAQ('finish', this.scope_module); end function pollTimerCallback(this) Data = ziDAQ('read', this.scope_module); if ziCheckPathInData(Data, [this.scope_path,'/wave']) % Get the list of scope waves recorded since the previous % poll new_waves = Data.(this.dev_id).scopes(this.n_scope).wave; % Add waves to the average trace for i=1:length(new_waves) dt = new_waves{i}.dt; n = double(new_waves{i}.totalsamples); % Calculate the frequency axis this.TmpTrace.x = linspace(0, (1-1/n)/(2*dt), n); this.TmpTrace.y = new_waves{i}.wave; is_compl = addAverage(this.Trace, this.TmpTrace); if is_compl && strcmpi(this.Trace.avg_type, 'lin') triggerNewData(this); end end notify(this, 'NewWave'); end end end methods (Access = protected) function createCommandList(this) addCommand(this, 'scope_rate', ... 'readFcn', @this.readScopeRate, ... 'writeFcn', @this.writeScopeRate, ... 'info', '(samples/s)'); addCommand(this, 'n_pt', ... 'readFcn', @this.readNpt, ... 'writeFcn', @this.writeNpt, ... 'info', 'Scope wave length'); end function createMetadata(this) createMetadata@MyZiLockIn(this); addObjProp(this.Metadata, this, 'n_scope', 'comment', ... 'Hardware scope number'); addObjProp(this.Metadata, this, 'n_ch', 'comment', ... 'Scope channel'); addObjProp(this.Metadata, this, 'signal_in', 'comment', ... 'Signal input number'); addObjProp(this.Metadata, this, 'trigholdoff', 'comment', ... ['(s), the scope hold off time inbetween acquiring ' ... 'triggers']); addParam(this.Metadata, 'poll_period', ... this.PollTimer.Period, 'comment', '(s)'); end function val = readScopeRate(this) tn = ziDAQ('getDouble', [this.scope_path '/time']); val = this.clockbase/(2^tn); end function writeScopeRate(this, val) tn = round(log2(this.clockbase/val)); % Trim to withn 0 and 16 tn = max(0,tn); tn = min(tn, 16); ziDAQ('setDouble', [this.scope_path '/time'], tn); clearData(this.Trace); end function val = readNpt(this) val = ziDAQ('getDouble', [this.scope_path '/length']); end function writeNpt(this, val) ziDAQ('setDouble', [this.scope_path '/length'], val); clearData(this.Trace); end end methods function val = get.scope_path(this) val = sprintf('/%s/scopes/%i',this.dev_id,this.n_scope-1); end function val = get.fft_rbw(this) l = length(this.Trace.x); if l>=2 val = this.Trace.x(2)-this.Trace.x(1); else val = Inf; end end function val = get.poll_period(this) val = this.PollTimer.Period; end end end