# HG changeset patch # User Daniel O'Connor # Date 1727911630 -34200 # Node ID 4b4ae555067b60db896baccb5e7675a8b9331e98 # Parent 4cc3d0706dd1ae39607e032bc7867a5ee3d5cfd9 Use RMS detector and fix the sweep time for more accuracy. Add links to app notes discussing theory. Run forever without pause and print a summary on ctrl-c diff -r 4cc3d0706dd1 -r 4b4ae555067b rs_fsp7_noisetest.py --- a/rs_fsp7_noisetest.py Thu Oct 03 08:56:07 2024 +0930 +++ b/rs_fsp7_noisetest.py Thu Oct 03 08:57:10 2024 +0930 @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (c) 2012 +# Copyright (c) 2024 # Daniel O'Connor . All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -25,9 +25,16 @@ # SUCH DAMAGE. # +# This script uses the "Y Factor Method" as discussed here: +# https://scdn.rohde-schwarz.com/ur/pws/dl_downloads/dl_application/application_notes/1ma178/1MA178_5e_NoiseFigure.pdf +# https://www.analog.com/en/resources/technical-articles/noise-figure-measurement-methods-and-formulas--maxim-integrated.html +# +# It requires an ENR and has hard coded values for the source we have. + +import argparse import math +import misc import numpy -import optparse import rsib import scipy import scpi @@ -42,143 +49,163 @@ enr = 10 ** (enrdb / 10) # Interpolate - rtn = scipy.interp([frq], enrfrq, enr) + rtn = numpy.interp([frq], enrfrq, enr) # Convert to dB - rtndb = 10 * math.log10(rtn) + rtndb = 10 * math.log10(rtn[0]) return rtndb -def setup(r, freq, span, sweeps, bw): +def setup(r, freq, span, sweeps, bw, atten, time): # Reset to defaults - r.write("*RST") + r.write('*RST') # Set to single sweep mode - r.write("INIT:CONT OFF") + r.write('INIT:CONT OFF') # Enable display updates - r.write("SYST:DISP:UPD ON") + r.write('SYST:DISP:UPD ON') # Set frequency range - r.write("SENSE1:FREQ:CENT %f Hz" % (freq)) - r.write("SENSE1:FREQ:SPAN %f Hz" % (span)) + r.write('SENSE1:FREQ:CENT %f Hz' % (freq)) + r.write('SENSE1:FREQ:SPAN %f Hz' % (span)) # Switch marker 1 on in screen A - r.write("CALC:MARK1 ON") + r.write('CALC:MARK1 ON') + + # Set marker to centre frequency + r.write('CALC:MARK1:X %f Hz ' % (freq)) # Enable noise measurement - r.write("CALC:MARK1:FUNC:NOIS ON") + r.write('CALC:MARK1:FUNC:NOIS ON') # Turn averaging on - r.write("AVER:STAT ON") + r.write('AVER:STAT ON') # Set number of sweeps - r.write("SWE:COUN %d" % (sweeps)) + r.write('SWE:COUN %d' % (sweeps)) + + # Use RMS detector + r.write('SENS:DET RMS') + + # Set sweep time + r.write('SWE:TIME %f' % (time)) # Set resolution bandwidth - r.write("SENS1:BAND:RES %f Hz" % (bw)) + r.write('SENS1:BAND:RES %f Hz' % (bw)) # Set video bandwidth (10x res BW) - r.write("SENS1:BAND:VID %f Hz" % (bw * 10)) + r.write('SENS1:BAND:VID %f Hz' % (bw * 10)) + + r.write('INP:ATT %s' % atten) def getnoise(r): # Trigger the sweep - r.write("INIT;*WAI") + r.write('INIT;*WAI') # Wait for it to be done - r.write("*OPC?") - opc = scpi.getdata(r.read(None), int) - #print "OPC - %d" % (opc) - assert(opc == 1) + r.ask('*OPC?') # Set data format - r.write("FORM:DATA ASC") + r.write('FORM:DATA ASC') # Read noise value - r.write("CALC:MARK1:FUNC:NOIS:RES?") - data = r.read(10) - #print "Data - " + data + data = r.ask('CALC:MARK1:FUNC:NOIS:RES?') + #print 'Data - ' + data return float(data) def setnoise(r, en): if en: - val = "ON" + val = 'ON' else: - val = "OFF" - r.write("DIAG:SERV:NSO " + val) + val = 'OFF' + r.write('DIAG:SERV:NSO ' + val) def calcnf(enrdb, offdb, ondb): # Not possible but noisy results may result in it happening if ondb <= offdb: - return 0 + return None ydb = ondb - offdb y = 10 ** (ydb / 10) enr = 10 ** (enrdb / 10) nf = 10 * math.log10(enr / (y - 1)) return nf -def donoisetest(r, enr): - print("Acquiring with noise off..") +def donoisetest(r, enr, sweeps): + swt = float(r.ask('SWE:TIME?')) + print('Acquiring with noise off.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps)) setnoise(r, False) off = getnoise(r) - print("Acquiring with noise on..") + print('Acquiring with noise on.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps)) setnoise(r, True) on = getnoise(r) - return off, on, calcnf(enr, off, on) + nf = calcnf(enr, off, on) + #print(nf) + return off, on, nf if __name__ == '__main__': - parser = optparse.OptionParser(usage = '%prog [options] address frequency', - description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test', - epilog = 'video bandwidth is set to 10 times the resolution bandwidth') - parser.add_option('-s', '--span', dest = 'span', default = 1e6, help = 'Span frequency in Hz (default: %default)', type = float) - parser.add_option('-i', '--input', dest = 'input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float) - parser.add_option('-w', '--sweeps', dest = 'sweeps', default = 20, help = 'Number of sweeps (default: %default)', type = int) - parser.add_option('-b', '--bw', dest = 'bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %default)', type = float) - parser.add_option('-r', '--repeat', dest = 'repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int) - - (options, args) = parser.parse_args() + parser = argparse.ArgumentParser(description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test', + epilog = 'Note: video bandwidth is set to 10 times the resolution bandwidth') + parser.add_argument('-a', '--atten', default = 10, help = 'Input attenuation in dB (default: %(default).0f dB)', type = float) + parser.add_argument('-b', '--bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %(default).1f Hz)', type = float) + parser.add_argument('-i', '--input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float) + parser.add_argument('-p', '--pause', default = False, action = 'store_true', + help = 'Wait between measurements (when not doing N repeats, default: %(default)s)') + parser.add_argument('-r', '--repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int) + parser.add_argument('-s', '--span', default = 1e6, help = 'Span frequency in Hz (default: %(default).0f Hz)', type = float) + parser.add_argument('-t', '--time', default = 30, help = 'Sweep time (default: %(default)f sec)', type = float) + parser.add_argument('-w', '--sweeps', default = 3, help = 'Number of sweeps to average (default: %(default)d)', type = int) + parser.add_argument('address', help = 'Spectrum analyser address', type = str) + parser.add_argument('centre', help = 'Centre frequency (Hz)', type = float) - if len(args) != 2: - parser.error('Must supply the specan address and centre frequency') + args = parser.parse_args() - addr = args[0] - try: - freq = float(args[1]) - except ValueError: - parser.error('Unable to parse frequency') + if args.input == None: + args.input = args.centre - if options.input == None: - options.input = freq + if args.time is not None and args.time <= 0: + parser.error('Sweep time must be >0') # Compute ENR at frequency of interest - enr = findenr(options.input) + enr = findenr(args.input) # Connect to the analyser - r = rsib.RSIBDevice(addr) + r = rsib.RSIBDevice(args.address) # ID instrument - r.write('*IDN?') - print("ID is " + r.read(5)) + print('Device ID is ' + r.ask('*IDN?').decode('ascii')) # Setup parameters - setup(r, freq, options.span, options.sweeps, options.bw) - - r.write("INIT:CONT OFF") + setup(r, args.centre, args.span, args.sweeps, args.bw, args.atten, args.time) nfs = [] - print("Centre: %.1f Mhz, Span %.1f Mhz, Input %.1f MHz, BW %.1f kHz, %d sweeps, ENR %.2f dB" % (freq / 1e6, options.span / 1e6, options.input / 1e6, options.bw / 1e3, options.sweeps, enr)) - while options.repeat == None or options.repeat > 0: - off, on, nf = donoisetest(r, enr) - print("Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %.2f dB" % (off, on, nf)) - nfs.append(nf) - if options.repeat == None: - print("Press enter to perform a new measurement") - sys.stdin.readline() - else: - options.repeat -= 1 + print('Centre: %s, Span %s, Input %s, RBW %s, Sweeps: %d, Sweep time: %.1f sec, ENR %.2f dB' % ( + misc.sifmt(args.centre), misc.sifmt(args.span), + misc.sifmt(args.input), misc.sifmt(args.bw), + args.sweeps, args.time, enr)) + while args.repeat == None or args.repeat > 0: + try: + off, on, nf = donoisetest(r, enr, args.sweeps) + if nf is None or nf < 0: + nfstr = 'Invalid' + else: + nfstr = '%.2f dB' % (nf) + nfs.append(nf) + print('Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %s' % (off, on, nfstr)) + if args.repeat == None: + if args.pause: + print('Press enter to perform a new measurement') + sys.stdin.readline() + else: + args.repeat -= 1 + except KeyboardInterrupt: + print('') + break if len(nfs) > 1: nfs = numpy.array(nfs) - print("NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/hz, stddev: %.1f" % ( - nfs.min(), nfs.max(), nfs.sum() / len(nfs), numpy.std(nfs))) + + # XXX: not sure mean/std of dBm/Hz values is really meaningful + print('N: %d, NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/Hz, stddev: %.1f' % ( + len(nfs), nfs.min(), nfs.max(), nfs.mean(), numpy.std(nfs)))