comparison 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
comparison
equal deleted inserted replaced
83:4cc3d0706dd1 84:4b4ae555067b
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 2
3 # Copyright (c) 2012 3 # Copyright (c) 2024
4 # Daniel O'Connor <darius@dons.net.au>. All rights reserved. 4 # Daniel O'Connor <darius@dons.net.au>. All rights reserved.
5 # 5 #
6 # Redistribution and use in source and binary forms, with or without 6 # Redistribution and use in source and binary forms, with or without
7 # modification, are permitted provided that the following conditions 7 # modification, are permitted provided that the following conditions
8 # are met: 8 # are met:
23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 23 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 24 # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
25 # SUCH DAMAGE. 25 # SUCH DAMAGE.
26 # 26 #
27 27
28 # This script uses the "Y Factor Method" as discussed here:
29 # https://scdn.rohde-schwarz.com/ur/pws/dl_downloads/dl_application/application_notes/1ma178/1MA178_5e_NoiseFigure.pdf
30 # https://www.analog.com/en/resources/technical-articles/noise-figure-measurement-methods-and-formulas--maxim-integrated.html
31 #
32 # It requires an ENR and has hard coded values for the source we have.
33
34 import argparse
28 import math 35 import math
36 import misc
29 import numpy 37 import numpy
30 import optparse
31 import rsib 38 import rsib
32 import scipy 39 import scipy
33 import scpi 40 import scpi
34 import sys 41 import sys
35 42
40 47
41 # Convert back to linear values 48 # Convert back to linear values
42 enr = 10 ** (enrdb / 10) 49 enr = 10 ** (enrdb / 10)
43 50
44 # Interpolate 51 # Interpolate
45 rtn = scipy.interp([frq], enrfrq, enr) 52 rtn = numpy.interp([frq], enrfrq, enr)
46 53
47 # Convert to dB 54 # Convert to dB
48 rtndb = 10 * math.log10(rtn) 55 rtndb = 10 * math.log10(rtn[0])
49 56
50 return rtndb 57 return rtndb
51 58
52 def setup(r, freq, span, sweeps, bw): 59 def setup(r, freq, span, sweeps, bw, atten, time):
53 # Reset to defaults 60 # Reset to defaults
54 r.write("*RST") 61 r.write('*RST')
55 62
56 # Set to single sweep mode 63 # Set to single sweep mode
57 r.write("INIT:CONT OFF") 64 r.write('INIT:CONT OFF')
58 65
59 # Enable display updates 66 # Enable display updates
60 r.write("SYST:DISP:UPD ON") 67 r.write('SYST:DISP:UPD ON')
61 68
62 # Set frequency range 69 # Set frequency range
63 r.write("SENSE1:FREQ:CENT %f Hz" % (freq)) 70 r.write('SENSE1:FREQ:CENT %f Hz' % (freq))
64 r.write("SENSE1:FREQ:SPAN %f Hz" % (span)) 71 r.write('SENSE1:FREQ:SPAN %f Hz' % (span))
65 72
66 # Switch marker 1 on in screen A 73 # Switch marker 1 on in screen A
67 r.write("CALC:MARK1 ON") 74 r.write('CALC:MARK1 ON')
75
76 # Set marker to centre frequency
77 r.write('CALC:MARK1:X %f Hz ' % (freq))
68 78
69 # Enable noise measurement 79 # Enable noise measurement
70 r.write("CALC:MARK1:FUNC:NOIS ON") 80 r.write('CALC:MARK1:FUNC:NOIS ON')
71 81
72 # Turn averaging on 82 # Turn averaging on
73 r.write("AVER:STAT ON") 83 r.write('AVER:STAT ON')
74 84
75 # Set number of sweeps 85 # Set number of sweeps
76 r.write("SWE:COUN %d" % (sweeps)) 86 r.write('SWE:COUN %d' % (sweeps))
87
88 # Use RMS detector
89 r.write('SENS:DET RMS')
90
91 # Set sweep time
92 r.write('SWE:TIME %f' % (time))
77 93
78 # Set resolution bandwidth 94 # Set resolution bandwidth
79 r.write("SENS1:BAND:RES %f Hz" % (bw)) 95 r.write('SENS1:BAND:RES %f Hz' % (bw))
80 96
81 # Set video bandwidth (10x res BW) 97 # Set video bandwidth (10x res BW)
82 r.write("SENS1:BAND:VID %f Hz" % (bw * 10)) 98 r.write('SENS1:BAND:VID %f Hz' % (bw * 10))
99
100 r.write('INP:ATT %s' % atten)
83 101
84 def getnoise(r): 102 def getnoise(r):
85 # Trigger the sweep 103 # Trigger the sweep
86 r.write("INIT;*WAI") 104 r.write('INIT;*WAI')
87 105
88 # Wait for it to be done 106 # Wait for it to be done
89 r.write("*OPC?") 107 r.ask('*OPC?')
90 opc = scpi.getdata(r.read(None), int)
91 #print "OPC - %d" % (opc)
92 assert(opc == 1)
93 108
94 # Set data format 109 # Set data format
95 r.write("FORM:DATA ASC") 110 r.write('FORM:DATA ASC')
96 111
97 # Read noise value 112 # Read noise value
98 r.write("CALC:MARK1:FUNC:NOIS:RES?") 113 data = r.ask('CALC:MARK1:FUNC:NOIS:RES?')
99 data = r.read(10) 114 #print 'Data - ' + data
100 #print "Data - " + data
101 115
102 return float(data) 116 return float(data)
103 117
104 def setnoise(r, en): 118 def setnoise(r, en):
105 if en: 119 if en:
106 val = "ON" 120 val = 'ON'
107 else: 121 else:
108 val = "OFF" 122 val = 'OFF'
109 r.write("DIAG:SERV:NSO " + val) 123 r.write('DIAG:SERV:NSO ' + val)
110 124
111 def calcnf(enrdb, offdb, ondb): 125 def calcnf(enrdb, offdb, ondb):
112 # Not possible but noisy results may result in it happening 126 # Not possible but noisy results may result in it happening
113 if ondb <= offdb: 127 if ondb <= offdb:
114 return 0 128 return None
115 ydb = ondb - offdb 129 ydb = ondb - offdb
116 y = 10 ** (ydb / 10) 130 y = 10 ** (ydb / 10)
117 enr = 10 ** (enrdb / 10) 131 enr = 10 ** (enrdb / 10)
118 nf = 10 * math.log10(enr / (y - 1)) 132 nf = 10 * math.log10(enr / (y - 1))
119 return nf 133 return nf
120 134
121 def donoisetest(r, enr): 135 def donoisetest(r, enr, sweeps):
122 print("Acquiring with noise off..") 136 swt = float(r.ask('SWE:TIME?'))
137 print('Acquiring with noise off.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps))
123 setnoise(r, False) 138 setnoise(r, False)
124 off = getnoise(r) 139 off = getnoise(r)
125 print("Acquiring with noise on..") 140 print('Acquiring with noise on.. (%.1f x %d = %.1f sec)' % (swt, sweeps, swt * sweeps))
126 setnoise(r, True) 141 setnoise(r, True)
127 on = getnoise(r) 142 on = getnoise(r)
128 return off, on, calcnf(enr, off, on) 143 nf = calcnf(enr, off, on)
144 #print(nf)
145 return off, on, nf
129 146
130 if __name__ == '__main__': 147 if __name__ == '__main__':
131 parser = optparse.OptionParser(usage = '%prog [options] address frequency', 148 parser = argparse.ArgumentParser(description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test',
132 description = 'Configures a Rohde Schwarz FSP7 spectrum analyser to do a noise figure test', 149 epilog = 'Note: video bandwidth is set to 10 times the resolution bandwidth')
133 epilog = 'video bandwidth is set to 10 times the resolution bandwidth') 150 parser.add_argument('-a', '--atten', default = 10, help = 'Input attenuation in dB (default: %(default).0f dB)', type = float)
134 parser.add_option('-s', '--span', dest = 'span', default = 1e6, help = 'Span frequency in Hz (default: %default)', type = float) 151 parser.add_argument('-b', '--bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %(default).1f Hz)', type = float)
135 parser.add_option('-i', '--input', dest = 'input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float) 152 parser.add_argument('-i', '--input', default = None, help = 'Frequency used to compute ENR (defaults to frequency)', type = float)
136 parser.add_option('-w', '--sweeps', dest = 'sweeps', default = 20, help = 'Number of sweeps (default: %default)', type = int) 153 parser.add_argument('-p', '--pause', default = False, action = 'store_true',
137 parser.add_option('-b', '--bw', dest = 'bw', default = 1000, help = 'Resolution bandwidth in Hz (default: %default)', type = float) 154 help = 'Wait between measurements (when not doing N repeats, default: %(default)s)')
138 parser.add_option('-r', '--repeat', dest = 'repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int) 155 parser.add_argument('-r', '--repeat', help = 'Number of repetitions, if not specified do one and ask to continue', type = int)
139 156 parser.add_argument('-s', '--span', default = 1e6, help = 'Span frequency in Hz (default: %(default).0f Hz)', type = float)
140 (options, args) = parser.parse_args() 157 parser.add_argument('-t', '--time', default = 30, help = 'Sweep time (default: %(default)f sec)', type = float)
141 158 parser.add_argument('-w', '--sweeps', default = 3, help = 'Number of sweeps to average (default: %(default)d)', type = int)
142 if len(args) != 2: 159 parser.add_argument('address', help = 'Spectrum analyser address', type = str)
143 parser.error('Must supply the specan address and centre frequency') 160 parser.add_argument('centre', help = 'Centre frequency (Hz)', type = float)
144 161
145 addr = args[0] 162 args = parser.parse_args()
146 try: 163
147 freq = float(args[1]) 164 if args.input == None:
148 except ValueError: 165 args.input = args.centre
149 parser.error('Unable to parse frequency') 166
150 167 if args.time is not None and args.time <= 0:
151 if options.input == None: 168 parser.error('Sweep time must be >0')
152 options.input = freq
153 169
154 # Compute ENR at frequency of interest 170 # Compute ENR at frequency of interest
155 enr = findenr(options.input) 171 enr = findenr(args.input)
156 172
157 # Connect to the analyser 173 # Connect to the analyser
158 r = rsib.RSIBDevice(addr) 174 r = rsib.RSIBDevice(args.address)
159 175
160 # ID instrument 176 # ID instrument
161 r.write('*IDN?') 177 print('Device ID is ' + r.ask('*IDN?').decode('ascii'))
162 print("ID is " + r.read(5))
163 178
164 # Setup parameters 179 # Setup parameters
165 setup(r, freq, options.span, options.sweeps, options.bw) 180 setup(r, args.centre, args.span, args.sweeps, args.bw, args.atten, args.time)
166
167 r.write("INIT:CONT OFF")
168 181
169 nfs = [] 182 nfs = []
170 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)) 183 print('Centre: %s, Span %s, Input %s, RBW %s, Sweeps: %d, Sweep time: %.1f sec, ENR %.2f dB' % (
171 while options.repeat == None or options.repeat > 0: 184 misc.sifmt(args.centre), misc.sifmt(args.span),
172 off, on, nf = donoisetest(r, enr) 185 misc.sifmt(args.input), misc.sifmt(args.bw),
173 print("Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %.2f dB" % (off, on, nf)) 186 args.sweeps, args.time, enr))
174 nfs.append(nf) 187 while args.repeat == None or args.repeat > 0:
175 if options.repeat == None: 188 try:
176 print("Press enter to perform a new measurement") 189 off, on, nf = donoisetest(r, enr, args.sweeps)
177 sys.stdin.readline() 190 if nf is None or nf < 0:
178 else: 191 nfstr = 'Invalid'
179 options.repeat -= 1 192 else:
193 nfstr = '%.2f dB' % (nf)
194 nfs.append(nf)
195 print('Off %.3f dBm/Hz, on %.3f dBm/Hz, NF %s' % (off, on, nfstr))
196 if args.repeat == None:
197 if args.pause:
198 print('Press enter to perform a new measurement')
199 sys.stdin.readline()
200 else:
201 args.repeat -= 1
202 except KeyboardInterrupt:
203 print('')
204 break
180 205
181 if len(nfs) > 1: 206 if len(nfs) > 1:
182 nfs = numpy.array(nfs) 207 nfs = numpy.array(nfs)
183 print("NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/hz, stddev: %.1f" % ( 208
184 nfs.min(), nfs.max(), nfs.sum() / len(nfs), numpy.std(nfs))) 209 # XXX: not sure mean/std of dBm/Hz values is really meaningful
210 print('N: %d, NF min: %.1f dBm/Hz, max: %.1f dBm/Hz, avg: %.1f dBm/Hz, stddev: %.1f' % (
211 len(nfs), nfs.min(), nfs.max(), nfs.mean(), numpy.std(nfs)))