diff --git a/JenkinsfileHW b/JenkinsfileHW index e72be7af..f2cdbe79 100644 --- a/JenkinsfileHW +++ b/JenkinsfileHW @@ -17,7 +17,7 @@ lock(label: 'adgt_test_harness_boards', quantity: 1){ // harness.set_env('telemetry_repo', 'http://gateway.englab:3000/mirrors/telemetry.git') // harness.set_env('telemetry_branch', 'master') harness.set_env('matlab_repo', 'https://github.com/analogdevicesinc/PrecisionToolbox.git') // Not necessary when using checkout scm - harness.set_env('matlab_release','R2021b') + harness.set_env('matlab_release','R2023b') harness.set_env('matlab_license','network') harness.set_matlab_timeout('30m') @@ -39,6 +39,7 @@ lock(label: 'adgt_test_harness_boards', quantity: 1){ harness.update_agents() //Set other test parameters + harness.set_env('docker_image', 'tfcollins/test-harness-ci-ubuntu-22_04:latest') harness.set_nebula_debug(true) harness.set_enable_docker(true) harness.set_docker_host_mode(true) @@ -63,8 +64,10 @@ lock(label: 'adgt_test_harness_boards', quantity: 1){ harness.add_stage(rebootBoard,'continueWhenFail') // Test stage - harness.set_matlab_commands(["addpath(genpath('test'))", + harness.set_matlab_commands(["addpath(genpath('/mlhsp/R2023b/toolbox/shared/libm2k'))", + "addpath(genpath('test'))", "pyenv('Version','/usr/bin/python3')", + "disp(clib.libm2k.libm2k.context.getVersion())", "runHWTests(getenv('board'))"]) harness.add_stage(harness.stage_library("MATLABTests"),'continueWhenFail') diff --git a/test/AD4630_24Tests.m b/test/AD4630_24Tests.m index ed2b9ec9..82b7984d 100644 --- a/test/AD4630_24Tests.m +++ b/test/AD4630_24Tests.m @@ -1,12 +1,23 @@ classdef AD4630_24Tests < HardwareTests - properties(TestParameter) - end properties uri = 'ip:analog-2.local'; author = 'ADI'; end - + + properties(TestParameter) + % start frequency, stop frequency, step, tolerance, repeats + signal_test = {{10000,250000,2500,0.05,10}}; + signal_vpp = {0.5}; + sample_rate = {'10000', '50000', '100000', ... + '200000', '500000', '1000000', ... + '1760000', '2000000'}; + sample_averaging_length = { ... + '2', '4', '8', '16', '32', '64', '128', '256', ... + '512', '1024', '2048', '4096', '8192', '16384', ... + '32768', '65536'}; + end + methods(TestClassSetup) % Check hardware connected function CheckForHardware(testCase) @@ -14,16 +25,130 @@ function CheckForHardware(testCase) testCase.CheckDevice('ip',Device,testCase.uri(4:end),false); end end + + methods (Static) + function freq = estFrequencyMax(data,fs) + nSamp = length(data); + FFTRxData = fftshift(10*log10(abs(fft(data)))); + df = fs/nSamp; freqRangeRx = (0:df:fs/2-df).'; + % Disregard DC + [~,ind] = maxk(FFTRxData(end-length(freqRangeRx)+1:end,:),2); + freq = max(freqRangeRx(ind)); + end + end methods (Test) function testAD4630_24Smoke(testCase) adc = adi.AD4630_24.Rx('uri',testCase.uri); - data = adc(); + [data,valid] = adc(); adc.release(); + testCase.assertTrue(valid); testCase.assertTrue(sum(abs(double(data)))>0); end - + + function testAD4630_24DifferentialInputRMS(testCase,signal_vpp) + % Signal source setup + m2k_class = instr_m2k(); + m2k = m2k_class.connect(getenv('M2K_URI'), false); + siggen = m2k_class.create_instr(m2k, "siggen"); + % ADC setup + adc = adi.AD4630_24.Rx('uri',testCase.uri); + fs = str2double(adc.SampleRate); + + % Set channel 1 to 50KHz, 500mHz and minimize channel 2 amplitude + signal_frequency = 5e4; + m2k_class.control(siggen, 0, [signal_frequency, signal_vpp, 0, 0]); + m2k_class.control(siggen, 1, [signal_frequency, 0.000001, 0, 0]); + for k = 1:5 + [data,valid] = adc(); + end + data = double(data); + % Generate same signal in channel 2 to double signal at differential inputs + m2k_class.control(siggen, 1, [signal_frequency, signal_vpp, 0, 0]); + for k = 1:5 + [data1,valid1] = adc(); + end + data1 = double(data1); + + % Clean up + adc.release(); + m2k_class.contextClose(); + + % Assertions + testCase.assertTrue(valid & valid1); + testCase.verifyEqual(rms(data1),rms(data)*2,'RelTol',0.3) + end + + function testAD4630_24DifferentialInputSweep(testCase,signal_test) + % Signal source setup + m2k_class = instr_m2k(); + m2k = m2k_class.connect(getenv('M2K_URI'), false); + siggen = m2k_class.create_instr(m2k, "siggen"); + % ADC setup + adc = adi.AD4630_24.Rx('uri',testCase.uri); + adc.EnabledChannels = [1,2]; + fs = str2double(adc.SampleRate); + + start = signal_test{1}; + stop = signal_test{2}; + step = signal_test{3}; + tol = signal_test{4}; + repeats = signal_test{5}; + numints = round((stop-start)/step); + for ii = 1:repeats + ind = randi([0, numints]); + frequency = start+(step*ind); + m2k_class.control(siggen, 0, [frequency, 0.5, 0, 0]); + m2k_class.control(siggen, 1, [frequency, 0.5, 0, 0]); + for k = 1:5 + [data,valid] = adc(); + end + data = double(data); + freq = testCase.estFrequencyMax(double(data),fs); + + % Assertions + testCase.assertTrue(valid); + testCase.verifyEqual(freq(1),freq(2),'RelTol',tol) + testCase.verifyEqual(mean(freq),frequency,'RelTol',tol,... + 'Frequency of signal unexpected') + end + + % Clean up + adc.release(); + m2k_class.contextClose(); + end + + function testAD4630_24AttrSampleRate(testCase,sample_rate) + % '1760000' is set as '1754386' + adc = adi.AD4630_24.Rx('uri',testCase.uri); + adc.uri = testCase.uri; + val = sample_rate; + adc.SampleRate = val; + [data,valid] = adc(); + ret_val = adc.getDeviceAttributeRAW('sampling_frequency',9); + adc.release(); + testCase.assertTrue(valid); + testCase.assertTrue(sum(abs(double(data)))>0); + % testCase.assertTrue(strcmp(val,string(ret_val)), ... + % 'Sample rate unexpected') + testCase.verifyEqual(str2double(ret_val),str2double(val), ... + 'RelTol',0.01,'Sample rate unexpected') + end + + % function testAD4630_24AttrSampleAveragingLength(testCase,sample_averaging_length) + % % The average mode works only with the output data mode set to 30-bit average + % % Skip test for now since it causes ERROR:READ LINE: -32 in iio_info + % adc = adi.AD4630_24.Rx('uri',testCase.uri); + % val = sample_averaging_length; + % adc.SampleAveragingLength = val; + % [data,valid] = adc(); + % ret_val = adc.getDeviceAttributeRAW('sample_averaging',8); + % adc.release(); + % testCase.assertTrue(valid); + % testCase.assertTrue(sum(abs(double(data)))>0); + % testCase.assertTrue(strcmp(val,string(ret_val))); + % end end end diff --git a/test/AD7768Tests.m b/test/AD7768Tests.m index e3e29449..60d86c52 100644 --- a/test/AD7768Tests.m +++ b/test/AD7768Tests.m @@ -1,11 +1,14 @@ classdef AD7768Tests < HardwareTests - - properties(TestParameter) - end + properties uri = 'ip:analog-2.local'; author = 'ADI'; end + + properties(TestParameter) + % start frequency. stop frequency, step, tolerance, repeats + signal_test = {{1000,100000,2500,0.025,10}}; + end methods(TestClassSetup) % Check hardware connected @@ -14,6 +17,17 @@ function CheckForHardware(testCase) testCase.CheckDevice('ip',Device,testCase.uri(4:end),false); end end + + methods (Static) + function freq = estFrequencyMax(data,fs) + nSamp = length(data); + FFTRxData = fftshift(10*log10(abs(fft(data)))); + df = fs/nSamp; freqRangeRx = (0:df:fs/2-df).'; + % Disregard DC + [~,ind] = maxk(FFTRxData(end-length(freqRangeRx)+1:end,:),2); + freq = max(freqRangeRx(ind)); + end + end methods (Test) @@ -24,6 +38,41 @@ function testAD7768Smoke(testCase) adc.release(); testCase.assertTrue(sum(abs(double(data)))>0); end + + function testAD7768Signal(testCase,signal_test) + % Signal source setup + m2k_class = instr_m2k(); + m2k = m2k_class.connect(getenv('M2K_URI'), false); + siggen = m2k_class.create_instr(m2k, "siggen"); + % ADC setup + adc = adi.AD7768.Rx; + adc.uri = testCase.uri; + adc.EnabledChannels = [1 2 3 4 5 6 7 8]; + + start = signal_test{1}; + stop = signal_test{2}; + step = signal_test{3}; + tol = signal_test{4}; + repeats = signal_test{5}; + numints = round((stop-start)/step); + for ii = 1:repeats + ind = randi([0, numints]); + frequency = start+(step*ind); + m2k_class.control(siggen, 0, [frequency, 0.5, 0.5, 0]); + m2k_class.control(siggen, 1, [frequency, 0.5, 0.5, 0]); + for k = 1:5 + data = adc(); + end + for ch = adc.EnabledChannels + freqEst = testCase.estFrequencyMax(data(:,ch),str2double(adc.SampleRate)); + testCase.assertTrue(sum(abs(double(data(:,ch))))>0); + testCase.verifyEqual(freqEst,frequency,'RelTol',tol,... + 'Frequency of signal unexpected') + end + end + adc.release(); + m2k_class.contextClose(); + end end diff --git a/test/AD7768_1Tests.m b/test/AD7768_1Tests.m index f1854eb2..15d0c8ce 100644 --- a/test/AD7768_1Tests.m +++ b/test/AD7768_1Tests.m @@ -1,12 +1,22 @@ classdef AD7768_1Tests < HardwareTests - - properties(TestParameter) - end + properties uri = 'ip:analog'; author = 'ADI'; end - + + properties(TestParameter) + % start. stop, step, tolerance, repeats + signal_test = {{1000,125000,2500,0.015,10}}; + samples_per_frame = {{2^1,2^20,2^16,0.0,10}}; + sample_rate = {'256000', '128000', '64000', ... + '32000', '16000', '8000', '4000', ... + '2000', '1000'}; + common_mode_voltage = { '(AVDD1-AVSS)/2','2V5', ... + '2V05','1V9','1V65','1V1','0V9', ... + 'OFF'}; + end + methods(TestClassSetup) % Check hardware connected function CheckForHardware(testCase) @@ -14,17 +24,119 @@ function CheckForHardware(testCase) testCase.CheckDevice('ip',Device,testCase.uri(4:end),false); end end + + methods (Static) + function freq = estFrequencyMax(data,fs) + nSamp = length(data); + FFTRxData = fftshift(10*log10(abs(fft(data)))); + df = fs/nSamp; freqRangeRx = (0:df:fs/2-df).'; + % Disregard DC + [~,ind] = maxk(FFTRxData(end-length(freqRangeRx)+1:end,:),2); + freq = max(freqRangeRx(ind)); + end + end methods (Test) function testAD7768_1Smoke(testCase) adc = adi.AD7768_1.Rx; adc.uri = testCase.uri; - data = adc(); + [data,valid] = adc(); adc.release(); + testCase.assertTrue(valid); testCase.assertTrue(sum(abs(double(data)))>0); end - + + function testAD7768_1Signal(testCase,signal_test) + % Signal source setup + m2k_class = instr_m2k(); + m2k = m2k_class.connect(getenv('M2K_URI'), false); + siggen = m2k_class.create_instr(m2k, "siggen"); + % ADC setup + adc = adi.AD7768_1.Rx; + adc.uri = testCase.uri; + + start = signal_test{1}; + stop = signal_test{2}; + step = signal_test{3}; + tol = signal_test{4}; + repeats = signal_test{5}; + numints = round((stop-start)/step); + for ii = 1:repeats + ind = randi([0, numints]); + frequency = start+(step*ind); + m2k_class.control(siggen, 0, [frequency, 0.5, 0, 0]); + for k = 1:5 + [data,valid] = adc(); + end + freqEst = testCase.estFrequencyMax(data,str2double(adc.SampleRate)); + testCase.assertTrue(valid); + testCase.assertTrue(sum(abs(double(data)))>0); + testCase.verifyEqual(freqEst,frequency,'RelTol',tol,... + 'Frequency of signal unexpected') + end + adc.release(); + m2k_class.contextClose(); + end + + function testAD7768_1AttrSamplesPerFrame(testCase,samples_per_frame) + % This is not written to the device. Should this even be tested? + adc = adi.AD7768_1.Rx; + adc.uri = testCase.uri; + + start = samples_per_frame{1}; + stop = samples_per_frame{2}; + step = samples_per_frame{3}; + tol = samples_per_frame{4}; + repeats = samples_per_frame{5}; + numints = floor((stop-start)/step); + for ii = 1:repeats + ind = randi([0, numints]); + val = start+(step*ind); + disp("Setting SamplesPerFrame to " + string(val)); + adc.SamplesPerFrame = val; + [data, valid] = adc(); + [ret_val,~] = size(data); + adc.release(); + testCase.assertTrue(valid); + testCase.assertTrue(sum(abs(double(data)))>0); + testCase.verifyEqual(ret_val,val,'RelTol',tol,... + 'Frequency of signal unexpected') + end + end + + function testAD7768_1AttrCommonModeVoltage(testCase,common_mode_voltage) + % FIXME: Hangs unless board is rebooted + adc = adi.AD7768_1.Rx; + adc.uri = testCase.uri; + val = common_mode_voltage; + disp("Setting CommonModeVoltage to " + val); + adc.CommonModeVolts = val; + [data,valid] = adc(); + ret_val = adc.getDeviceAttributeRAW('common_mode_voltage',16); + disp("Read attribute value: " + ret_val); + adc.release(); + testCase.assertTrue(valid); + testCase.assertTrue(sum(abs(double(data)))>0); + testCase.assertTrue(strcmp(val,string(ret_val))); + end + + function testAD7768_1AttrSampleRate(testCase,sample_rate) + % FIXME: Hangs unless board is rebooted + adc = adi.AD7768_1.Rx; + adc.uri = testCase.uri; + val = sample_rate; + disp("Setting SampleRate to " + val); + adc.SampleRate = val; + [data,valid] = adc(); + ret_val = adc.getDeviceAttributeRAW('sampling_frequency',8); + disp("Read attribute value: " + ret_val); + adc.release(); + testCase.assertTrue(valid); + testCase.assertTrue(sum(abs(double(data)))>0); + testCase.assertTrue(strcmp(val,string(ret_val))); + end + end end diff --git a/test/instr_m2k.m b/test/instr_m2k.m new file mode 100644 index 00000000..e7b62cc8 --- /dev/null +++ b/test/instr_m2k.m @@ -0,0 +1,174 @@ +classdef instr_m2k < handle + + properties + DAC_available_sample_rates = [750, 7500, 75000, 750000, 7500000, 75000000]; + DAC_max_rate + DAC_min_nr_of_points = 10; + max_buffer_size = 500000; + ADC_available_sample_rates = [1000, 10000, 100000, 1000000, 10000000, 100000000]; + ADC_max_rate + ADC_min_nr_of_points = 10; + end + + methods + function [best_ratio, best_fract] = get_best_ratio(obj, ratio) + max_it = obj.max_buffer_size / ratio; + best_ratio = ratio; + best_fract = 1; + + for i = 1 : fix(max_it) + new_ratio = i * ratio; + % (new_fract, integral) = math.modf(new_ratio) + new_fract = mod(abs(new_ratio),1); + % integral = fix(new_ratio); + if new_fract < best_fract + best_fract = new_fract; + best_ratio = new_ratio; + end + if new_fract == 0 + break + end + end + end + + function size = get_samples_count(obj, rate, freq, mode) + ratio = rate / freq; + if mode == "DAC" + min_nr_of_points = obj.DAC_min_nr_of_points; + max_rate = obj.DAC_max_rate; + elseif mode == "ADC" + min_nr_of_points = obj.ADC_min_nr_of_points; + max_rate = obj.ADC_max_rate; + end + + if ratio < min_nr_of_points && rate < max_rate + size = 0; + return + end + if ratio < 2 + size = 0; + return + end + + [ratio, fract] = obj.get_best_ratio(ratio); + % ratio = number of periods in buffer + % fract = what is left over - error + + size = fix(ratio); + while bitand(size, uint16(0x03)) & (size < 2^15) + size = size * 2; % double instead of python << + end + while size < 1024 + size = size * 2; % double instead of python << + end + end + + function rate = get_optimal_sample_rate(obj, freq, mode) + if mode == "DAC" + available_sample_rates = obj.DAC_available_sample_rates; + elseif mode == "ADC" + available_sample_rates = obj.ADC_available_sample_rates; + end + + for rate = available_sample_rates + buf_size = obj.get_samples_count(rate, freq, mode); + if buf_size + break + end + end + end + + function [sample_rate, buf] = sine_buffer_generator(obj, freq, ampl, offset, phase) + sample_rate = obj.get_optimal_sample_rate(freq, "DAC"); + nr_of_samples = obj.get_samples_count(sample_rate, freq, "DAC"); + samples_per_period = sample_rate / freq; + phase_in_samples = (phase / 360) * samples_per_period; + + buf = zeros(1,nr_of_samples); + + for i = 1:nr_of_samples + buf(i) = (... + offset ... + + ampl ... + * (sin(((i + phase_in_samples) / samples_per_period) * 2 * pi)) ... + ); + end + end + end + + + methods + % Constructor + function obj = instr_m2k() + obj.DAC_max_rate = obj.DAC_available_sample_rates(end); % last sample rate = max rate + obj.ADC_max_rate = obj.ADC_available_sample_rates(end); % last sample rate = max rate + end + + function m2k = connect(obj, uri, calibrate) + import clib.libm2k.libm2k.* + m2k = context.m2kOpen(uri); + if clibIsNull(m2k) + clib.libm2k.libm2k.context.contextCloseAll(uri); + m2k = context.m2kOpen(); + end + if isempty(m2k) + error('M2K device not found'); + end + if calibrate + m2k.calibrateDAC(); + m2k.calibrateADC(); + end + end + + % function m2k = calibrate() + % end + + function instr = create_instr(obj, ctx, instrument) + % Method to create instrument object from ctx + if instrument == "siggen" + instr = ctx.getAnalogOut(); + elseif instrument == "specanalyzer" | instrument == "voltmeter" + instr = ctx.getAnalogIn(); + elseif instrument == "powersupply" + instr = ctx.getPowerSupply(); + end + end + + function retval = control(obj, instr, chan, control_param) + % Generic control method for any instrument + % instr - instrument object + % chan - ADC or DAC channel, 0 or 1 only + % control_param - a list containing control parameters + % siggen - [tone_frequency,ampl,offset,phase] + % TODO: implement other instruments + % control_param should be treated as varargin + + if isa(instr, 'clib.libm2k.libm2k.analog.M2kAnalogOut') && length(control_param) == 4 + % Generates sinewave at chan + % Known issue - signal generated at one channel appears at the other + control_param = num2cell(control_param); + [tone_frequency, ampl, offset, phase] = control_param{:}; + [samp, buf] = obj.sine_buffer_generator(tone_frequency, ampl, offset, phase); + instr.enableChannel(chan, true); + instr.setSampleRate(chan, samp); + instr.push(chan, buf); + retval = 1; + end + end + + function contextClose(obj) + import clib.libm2k.libm2k.* + clib.libm2k.libm2k.context.contextCloseAll(); + end + + function delete(obj, m2k) + vars = who; + for i = 1 : length(vars) + if contains(class(eval(vars{i})),'clib.libm2k.libm2k.analog') + evalin('base', "clear " + string(vars{1})); + end + end + end + + end +end \ No newline at end of file