Mercurial > ~darius > hgwebdir.cgi > pyinst
view rs_fsp7_noisetest.py @ 84:4b4ae555067b
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
author | Daniel O'Connor <doconnor@gsoft.com.au> |
---|---|
date | Thu, 03 Oct 2024 08:57:10 +0930 |
parents | 23c96322cfb6 |
children | 60ad91b4c67c |
line wrap: on
line source
#!/usr/bin/env python # Copyright (c) 2024 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # 1. Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF # 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 rsib import scipy import scpi import sys def findenr(frq): # ENR values from the noise source enrdb = numpy.array([15.55, 15.96, 15.68, 15.11, 15.07, 14.84, 14.77, 14.82, 14.86, 14.79, 14.83, 14.93, 14.93, 15.07, 15.19, 15.08, 15.14, 14.87, 14.97, 14.59]) enrfrq = numpy.array([0.01e9, 0.1e9, 1.0e9, 2.0e9, 3.0e9, 4.0e9, 5.0e9, 6.0e9, 7.0e9, 8.0e9, 9.0e9, 10.0e9, 11.0e9, 12.0e9, 13.0e9, 14.0e9, 15.0e9, 16.0e9, 17.0e9, 18.0e9]) # Convert back to linear values enr = 10 ** (enrdb / 10) # Interpolate rtn = numpy.interp([frq], enrfrq, enr) # Convert to dB rtndb = 10 * math.log10(rtn[0]) return rtndb def setup(r, freq, span, sweeps, bw, atten, time): # Reset to defaults r.write('*RST') # Set to single sweep mode r.write('INIT:CONT OFF') # Enable display updates 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)) # Switch marker 1 on in screen A 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') # Turn averaging on r.write('AVER:STAT ON') # Set number of 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)) # Set video bandwidth (10x res BW) 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') # Wait for it to be done r.ask('*OPC?') # Set data format r.write('FORM:DATA ASC') # Read noise value data = r.ask('CALC:MARK1:FUNC:NOIS:RES?') #print 'Data - ' + data return float(data) def setnoise(r, en): if en: val = 'ON' else: 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 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, 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.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps)) setnoise(r, True) on = getnoise(r) nf = calcnf(enr, off, on) #print(nf) return off, on, nf if __name__ == '__main__': 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) args = parser.parse_args() if args.input == None: args.input = args.centre 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(args.input) # Connect to the analyser r = rsib.RSIBDevice(args.address) # ID instrument print('Device ID is ' + r.ask('*IDN?').decode('ascii')) # Setup parameters setup(r, args.centre, args.span, args.sweeps, args.bw, args.atten, args.time) nfs = [] 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) # 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)))