From 2548795837cad190890ef9dc3532421d4784fed5 Mon Sep 17 00:00:00 2001 From: Christian Schirmer Date: Fri, 28 Feb 2020 16:03:16 -0500 Subject: [PATCH] Initial Commit: Working code for DAQ, Sinusoidal Waveform Generation, FRA --- KS_DSOX1204G.m | 450 +++++++++++++++++++++++++++++++++++++++++++++++++ TestScript.m | 97 +++++++++++ 2 files changed, 547 insertions(+) create mode 100644 KS_DSOX1204G.m create mode 100644 TestScript.m diff --git a/KS_DSOX1204G.m b/KS_DSOX1204G.m new file mode 100644 index 0000000..3a6eaab --- /dev/null +++ b/KS_DSOX1204G.m @@ -0,0 +1,450 @@ +classdef KS_DSOX1204G + %% Class Properties + properties + % Instrument object + instr + + % Define Input Buffer Size + inputBufferSize = 2000000; + end + + %% Class Methods + methods + % Constructor + % ipAddress {String} where the oscilloscope can be located + function obj = KS_DSOX1204G(ipAddress) + % networkAdapter might need to be changed if moving to a + % different machine or changing the network adapter + % configuration. + networkAdapter = 'TCPIP0'; + connectionString = [networkAdapter,'::', ipAddress, '::hislip0::INSTR']; + + % Find a VISA_TCPIP object + obj.instr = instrfind('Type', 'visa-tcpip', 'RsrcName', connectionString , 'Tag', ''); + + % Create the VISA-TCPIP object if it does not exist + % otherwise use the object that was found. + if isempty(obj.instr) + obj.instr = visa('KEYSIGHT', connectionString); + else + fclose(obj.instr); + obj.instr = obj.instr(1); + end + + set(obj.instr, 'InputBufferSize', obj.inputBufferSize); + end + + % Connect and disconnect functionality + function obj = connect(obj) + % Connect to the instrument object + if(strcmp(obj.instr.Status,'closed')) + fopen(obj.instr); + end + end + + function obj = disconnect(obj) + % Disconnect from the instrument object + fclose(obj.instr); + end + + % Write and Query Functionality + function write(obj, msg) + obj.connect(); + fprintf(obj.instr, msg); + end + + function response = query(obj,msg) + obj.connect(); + response = query(obj.instr, msg); + end + + function resetScope(obj) + obj.connect(); + obj.write('*RST'); + end + + + % Acquisition Functionality + function data = captureChannels(obj, channels, timeRange, chanVoltRange) + % CAPTURECHANNELS(obj, channels, timeRange, chanVoltRange) + % This function performs data acquisition from the oscilloscope, on + % the channels specified in the channels argument. + % Input Arguments: + % channels: A vector containing all the channels to be + % acquired. Each channel must be a whole number of + % type double. At least 1 channel must be specified. + % Ex. [1 2], [1 2 3 4], etc. + % timeRange: A [double] specifying the number of [seconds] of + % data to capture. This effectively sets the range of + % the time vector captured over. As the scope can + % only handle so much data, the sampling rate will be + % affected, so small time ranges are preferred. + % NOTE: Specify an empty vector, [], to use the + % current horizontal scale set on the scope. + % chanVoltRange: + % A cell array containing the same number of elements + % as channels specified in the channels argument. + % Determines the voltage range for each channel to be + % acquired. Each cell can contain one of the + % following values: + % 1) 'auto' [string] - Uses the scope's automatic + % rangefinding functionality. + % 2) [min max] [1x2 double array] - Defines the lower and + % upper voltage bounds for the signal. + % 3) [] [empty double array] - Uses the range currently set + % for the channel on the scope + % Example: {'auto', [0, 4], []} + % The first channel is automatically scaled, the + % second channel has a range of 0V-4V, and the + % last channel will use whatever range is + % currently set on the scope. + % Output: + % The data variable is a struct containing the following data: + % Fs: Sampling frequency, samples per second + % Time_s: A vector containing sample times + % Chan_V: Vectors containing the voltage samples on channel + % + + % Ensure connection to the scope + obj.connect(); + + % INPUT SANITIZATION: + % [channels] argument + if(~isa(channels, 'double')) + error('"channels" input argument must be of type double.'); + end + if(0~=sum((channels<1)+(channels>4)+(channels~=floor(channels)))) + error('"channels" input argument may only contain integer values of 1,2,3, or 4.'); + end + % [timeRange] argument + if(~isa(timeRange, 'double')) + error('"timeRange" input argument must be of type double, or an empty vector'); + end + % [chanVoltRange] argument + if(~isa(chanVoltRange,'cell')) + error('"chanVoltRange" input argument must be a cell array.'); + end + if(length(chanVoltRange) ~= length(channels)) + error(['"chanVoltRange" input argument must have the same' ... + 'number of cells as input channels specified in the "channels" input argument.']); + end + for(curCell = chanVoltRange) + if(isa(curCell{1}, 'double') && (all(size(curCell{1})==[1,2]) || all(size(curCell{1})==[0,0]))) + continue; + elseif(isa(curCell{1},'char') && strcmp(curCell{1}, 'auto')) + continue; + else + error('Invalid input given for "chanVoltRange" input argument'); + end + end + + % Set acquisition mode to high resolution + obj.write('acquire:type hresolution'); + + % Set the waveform transfer format to be of type WORD (2 bytes + % per data point) + obj.write('waveform:format WORD'); + + % Retrieve data from Measurement Record (p. 647) + obj.write('waveform:points:mode Normal'); + + % Retrieve ALL data points (p. 645) + obj.write('waveform:points MAX'); + + % Returned data should be unsigned integers (p. 660) + obj.write('waveform:unsigned 1'); + + % Set the bytes mode + obj.write('waveform:byteorder LSBFirst'); + + % Set the Voltage Range Scaling + for i = 1:length(channels) + if(isa(chanVoltRange{i}, 'char') && strcmp(chanVoltRange{i},'auto')) + obj.write([':autoscale channel' num2str(channels(i),'%i')]); + elseif(length(chanVoltRange{i})==2) + vRange = chanVoltRange{i}(2)-chanVoltRange{i}(1); + vOffset = vRange/2 + chanVoltRange{i}(1); + obj.write([':Channel' num2str(channels(i),'%i') ':Range ' num2str(vRange,'%G') 'V']); + obj.write([':Channel' num2str(channels(i),'%i') ':Offset ' num2str(vOffset,'%G') 'V']); + end + end + + % Set the Time Range + obj.write(['Timebase:Range ' num2str(timeRange, '%G')]); + + % Build a string of all the channels to acquire + chans2acquire = ''; + for(curChannel = channels) + chans2acquire = [chans2acquire 'Channel' num2str(curChannel, '%i') ',']; + end + % Strip the trailing comma off the end of the string + chans2acquire = chans2acquire(1:end-1); + + % Perform the acquisition + obj.write([':digitize ' chans2acquire]); + + for(curChannel = channels) + % Compute the string value of the current channel. + curChanString = num2str(curChannel, '%i'); + % Select the channel to download from + obj.write([':waveform:source chan', curChanString]); + + % Download the data + obj.write(':waveform:data?'); + + + % First returned byte should be the character '#' + % See p. 47 of Programmer's Manual + if(not(strcmp(fscanf(obj.instr, '%c',1),'#'))) + disp("ACQUISITION ERROR: First byte returned not a '#'. Aborting ascquisition."); + return; + end + if(not(strcmp(fscanf(obj.instr, '%c',1),'8'))) + disp("ACQUISITION ERROR: Second byte returned not an '8'. Aborting ascquisition."); + return; + end + + % The next 8 characters determine the size of the dataset to be + % transferred. + numBytes = str2num(fscanf(obj.instr, '%c', 8)); + % Read numBytes of data (divide by 2 for uint16 data type: 2 + % bytes per uint16) + data.(['chan' curChanString '_raw']) = fread(obj.instr,numBytes/2,'uint16'); + + % Next, query for the preamble block (p.649) + obj.write(':Waveform:preamble?'); + preambleDataString = fscanf(obj.instr); + + % Process the collected data + data.preambleData = KS_DSOX1204G.processPreambleData(preambleDataString); + % Find the sampling frequency + data.Fs = 1/data.preambleData.x_increment; + % Convert Channel 1 signal to voltage. + data.(['Chan' curChanString '_V']) = KS_DSOX1204G.processChannelVoltageSignal(data.(['chan' curChanString '_raw']), data.preambleData); + + % Determine if the signal has clipped. + % Note: The Programmer's Guide lists the following clipping + % values on p. 642: + % Spec. Clipping Low: 0x0001 (0d1) + % Spec. Clipping High: 0xFFFF (0d65535) + % In practice, this seems to be incorrect. I obtained the + % following clipping values from using the scope: + % Actual Clipping Low: 240 + % Actual Clipping High: 65264 + % We will use these experimentally determined values to + % determine if the signal has clipped or not. + clip_low = min(data.(['chan' curChanString '_raw'])) <= 240; + clip_high = max(data.(['chan' curChanString '_raw'])) >= 65264; + data.Clipping.(['Chan' curChanString '_Low']) = clip_low; + data.Clipping.(['Chan' curChanString '_High']) = clip_high; + if(clip_low || clip_high) + warning(['Clipping has been detected in the captured signal for channel ' curChanString '.']); + end + + % Remove the raw data field + data = rmfield(data, ['chan' curChanString '_raw']); + + end + + % Compute the Timeseries + data.Time_s = (0:data.preambleData.points-1)'*data.preambleData.x_increment; + end + + % Waveform Generator Functionality + function wave_startSine(obj, freq, amplitude, offset) + % wave_startSine(freq, amplitude, offset) + % This function configures and starts the wave generator, producing + % a sine wave with the specified frequency, amplitude, and DC + % offset. + % Input Arguments: + % freq: The desired sine wave frequency, given as a double + % [double] in Hz. + % amplitude: The desired sine wave amplitude, given as a double + % [double] in Volts. + % offset: The desired DC voltage offset of the sine wave, + % [double] given as a double in Volts. + + % Ensure connection + obj.connect(); + % Reset to default settings + obj.write(':wgen:rst'); + % Set the wave generator to sine mode + obj.write(':wgen:function sinusoid'); + + % Set the Frequency + obj.write([':wgen:frequency ' num2str(freq, '%G')]); + actFreq = obj.query(':wgen:frequency?'); + if(freq ~= str2num(actFreq)) + warning(['The requested frequency of ' num2str(freq) '[Hz]' ... + ' was not successfully applied. The scope is using a ' ... + 'frequency value of ' actFreq '[Hz]']); + end + + % Set the Amplitude + obj.write([':wgen:voltage ' num2str(amplitude, '%G')]); + actAmplitude = obj.query(':wgen:voltage?'); + if(amplitude ~= str2num(actAmplitude)) + warning(['The requested amplitude of ' num2str(amplitude) '[V]' ... + ' was not successfully applied. The scope is using an ' ... + 'amplitude value of ' actAmplitude '[V]']); + end + + % Set the DC Offset + obj.write([':wgen:voltage:offset ' num2str(offset, '%G')]); + actOffset = (obj.query(':wgen:voltage:offset?')); + if(offset ~= str2num(actOffset)) + warning(['The requested offset of ' num2str(offset) '[V]' ... + ' was not successfully applied. The scope is using an ' ... + 'offset value of ' actOffset '[V]']); + end + + % Enable the waveform generator + obj.write(':wgen:output ON'); + end + + function wave_stop(obj) + % This function simply turns off the waveform generator. + + % Ensure connection + obj.connect(); + + % Stop the waveform + obj.write(':wgen:output OFF'); + end + + % Frequency Response Analysis Functionality + function data = fra_run(obj, fRange, points) + % fra_run(fRange, points) + % This function runs the frequency response analysis on the + % oscilloscope, returning the FRA data. + % Input Arguments: + % fRange: A a matrix containing the minimum and maximum + % [1x2 double] frequencies to be swept between [min, max] + % points: The total number of frequency points in the + % [doublw] sweep. This should be an integer. + ch_in = 1; % Input Scope Channel (System Input) + ch_out = 2; % Output Scope Channel (System Response) + timeout_s = 120; % Operation will timeout after this long + + % Enable FRA Mode + obj.write(':FRAnalysis:Enable 1'); + % Set to Sweep Mode + obj.write(':FRAnalysis:Frequency:Mode Sweep'); + + % Set the input and output channels + obj.write([':FRAnalysis:Source:Input Channel' num2str(ch_in,'%i')]); + obj.write([':FRAnalysis:Source:Output Channel' num2str(ch_out,'%i')]); + + + % Set the lower and upper bounds of the frequency sweep range + obj.write([':FRAnalysis:Frequency:Start ' num2str(fRange(1),'%G')]); + obj.write([':FRAnalysis:Frequency:Stop ' num2str(fRange(2),'%G')]); + + % Set the number of frequency points + obj.write([':FRAnalysis:Sweep:Points ' num2str(points, '%i')]); + + + % Check ESR to clear prior to running analysis + obj.checkESR; + % Run the analysis + obj.write(':FRAnalysis:RUN'); + + % Block until complete (Check ESR Register, bit 0) + elapsed = 0; + while(elapsed