changeset 84:4b4ae555067b default tip

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 4cc3d0706dd1
children
files rs_fsp7_noisetest.py
diffstat 1 files changed, 98 insertions(+), 71 deletions(-) [+]
line wrap: on
line diff
--- 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 <darius@dons.net.au>.  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)))