comparison fsp7_phasenoise.py @ 81:1947d10f9395

- Search for signal frequency iteratively to improve accuracy. - Use narrower bandwidths (via FFT filter) for close in measurements. - Work around bug in firmware when using FFT filter. - Actually _raise_ the exception when the signal power is too low.
author Daniel O'Connor <doconnor@gsoft.com.au>
date Fri, 27 Sep 2024 16:56:44 +0930
parents 23c96322cfb6
children 6ce8ed5ba76d
comparison
equal deleted inserted replaced
80:b280fe3b696a 81:1947d10f9395
25 # SUCH DAMAGE. 25 # SUCH DAMAGE.
26 # 26 #
27 27
28 import argparse 28 import argparse
29 import math 29 import math
30 import misc
30 import numpy 31 import numpy
31 import os 32 import os
32 import PIL 33 import PIL
33 import rsib 34 import rsib
34 import scipy 35 import scipy
35 import scpi 36 import scpi
36 import sys 37 import sys
37 import time 38 import time
38
39 def sifmt(_v, dp = 3, unit = 'Hz', sp = ' '):
40 '''Format a number using SI prefixes'''
41 si_prefixes = ('T', 'G', 'M', 'k', '', 'm', 'ยต', 'n', 'p')
42 scale = 10 ** 12
43 v = abs(_v)
44 if v == 0:
45 sip = ""
46 scale = 0
47 for i, sip in enumerate(si_prefixes):
48 if v >= scale:
49 break
50 scale /= 1e3
51 return ('%.' + str(dp) + 'f%s%s%s') % (_v / scale, sp, si_prefixes[i], unit)
52 39
53 def setupsweep(r, rbw, vbw, centre = None, span = None, start = None, stop = None): 40 def setupsweep(r, rbw, vbw, centre = None, span = None, start = None, stop = None):
54 '''Helper function to set various sweep parameters''' 41 '''Helper function to set various sweep parameters'''
55 if centre is not None: 42 if centre is not None:
56 r.write('SENSE1:FREQ:CENT %f Hz' % (centre)) 43 r.write('SENSE1:FREQ:CENT %f Hz' % (centre))
59 if start is not None: 46 if start is not None:
60 r.write('SENSE1:FREQ:START %f Hz' % (start)) 47 r.write('SENSE1:FREQ:START %f Hz' % (start))
61 if stop is not None: 48 if stop is not None:
62 r.write('SENSE1:FREQ:STOP %f Hz' % (stop)) 49 r.write('SENSE1:FREQ:STOP %f Hz' % (stop))
63 if rbw is not None: 50 if rbw is not None:
51 # Need to use FFT filters below 10Hz BW
52 if rbw < 10:
53 r.write('BAND:TYPE FFT')
54 else:
55 r.write('BAND:TYPE NORM')
64 r.write('SENS1:BAND:RES %f Hz' % (rbw)) 56 r.write('SENS1:BAND:RES %f Hz' % (rbw))
65 if vbw is not None: 57 if vbw is not None:
66 r.write('SENS1:BAND:VID %f Hz' % (vbw)) 58 r.write('SENS1:BAND:VID %f Hz' % (vbw))
67 59
68 def dosweep(r, sweeps): 60 def dosweep(r, sweeps):
69 '''Helper function to trigger a sweep and wait for it to finish''' 61 '''Helper function to trigger a sweep and wait for it to finish'''
70 swt = float(r.ask('SWE:TIME?')) 62 tout = getsweeptime(r) * 5 * sweeps
71 tout = swt * 5 * sweeps
72 if tout < 1: 63 if tout < 1:
73 tout = 1 64 tout = 1
74 #print('Sweep time', swt) 65 #print('Sweep time', swt)
75 66
76 # Trigger the sweep 67 # Trigger the sweep
97 peaks_pwr.append(pwr) 88 peaks_pwr.append(pwr)
98 r.write('CALC:MARK1:MAX:NEXT') 89 r.write('CALC:MARK1:MAX:NEXT')
99 90
100 return peaks_f, peaks_pwr 91 return peaks_f, peaks_pwr
101 92
93 def getsweeptime(r):
94 '''Return sweep time in seconds'''
95 # According to the manual SWE:TIME works for both normal and FFT filters
96 # however in practise it doesn't get a response for FFT ones so we fake it
97
98 if r.ask('BAND:TYPE?').decode('ascii') == 'FFT':
99 return 5
100 else:
101 return float(r.ask('SWE:TIME?'))
102
102 def phasenoise(r, nominal, sweeps, atten, rlev, yscale, ssprefix): 103 def phasenoise(r, nominal, sweeps, atten, rlev, yscale, ssprefix):
103 '''Main function to find a signal, do some phase noise measurements and wideband sweeps''' 104 '''Main function to find a signal, do some phase noise measurements and wideband sweeps'''
104 cpwrlim = -30 105 cpwrlim = -30
105 measurements = ( 106 measurements = (
106 { 'offset' : 10, 'span' : 100, 'rbw' : 10, 'vbw' : 10 }, 107 { 'offset' : 10, 'span' : 100, 'rbw' : 1, 'vbw' : 3 },
107 { 'offset' : 100, 'span' : 250, 'rbw' : 10, 'vbw' : 10 }, 108 { 'offset' : 100, 'span' : 250, 'rbw' : 1, 'vbw' : 3 },
108 { 'offset' : 1e3, 'span' : 2.5e3, 'rbw' : 10, 'vbw' : 30 }, 109 { 'offset' : 1e3, 'span' : 2.5e3, 'rbw' : 1, 'vbw' : 3 },
109 { 'offset' : 10e3, 'span' : 25e3, 'rbw' : 100, 'vbw' : 300 }, 110 { 'offset' : 10e3, 'span' : 25e3, 'rbw' : 100, 'vbw' : 300 },
110 { 'offset' : 100e3, 'span' : 250e3, 'rbw' : 100, 'vbw' : 300 }, 111 { 'offset' : 100e3, 'span' : 250e3, 'rbw' : 100, 'vbw' : 300 },
111 { 'offset' : 1e6, 'span' : 2.1e6, 'rbw' : 1e3, 'vbw' : 3e3 }, 112 { 'offset' : 1e6, 'span' : 2.1e6, 'rbw' : 1e3, 'vbw' : 3e3 },
112 ) 113 )
113 # Reset to defaults 114 # Reset to defaults
114 r.write('*RST') 115 r.write('*RST')
116 r.write('*CLS')
115 117
116 # Set to single sweep mode 118 # Set to single sweep mode
117 r.write('INIT:CONT OFF') 119 r.write('INIT:CONT OFF')
118 120
119 # Enable display updates 121 # Enable display updates
134 r.write('DISP:WIND:TRAC:Y:RLEV AUTO') 136 r.write('DISP:WIND:TRAC:Y:RLEV AUTO')
135 else: 137 else:
136 r.write('DISP:WIND:TRAC:Y:RLEV %f' % (rlev)) 138 r.write('DISP:WIND:TRAC:Y:RLEV %f' % (rlev))
137 139
138 # 140 #
139 # Look for signal 141 # Look from 100kHz down to 100Hz to narrow down the exact frequency
140 # 142 #
141 # Set frequency range etc 143 found = False
142 setupsweep(r, 10, 30, centre = nominal, span = 1e3) 144 cmeas = nominal
143 145 for idx, span in enumerate((100e3, 10e3, 1e3, 100)):
144 # Switch marker 1 on in screen A 146 setupsweep(r, span / 50, span / 50 * 3, centre = cmeas, span = span)
145 r.write('CALC:MARK1 ON') 147 print('Looking for signal at %s+/-%s (%.1f seconds)' % (
146 print('Looking for signal (%.1f seconds)' % (float(r.ask('SWE:TIME?')))) 148 misc.sifmt(nominal), misc.sifmt(span, dp = 1),
147 149 getsweeptime(r)))
148 # Do the sweep 150 # Do the sweep
149 dosweep(r, 1) 151 dosweep(r, 1)
150 152
151 # Check the instrument is happy 153 # Check the instrument is happy
152 status = int(r.ask('STAT:QUES:COND?')) 154 status = int(r.ask('STAT:QUES:COND?'))
153 pwrstat = int(r.ask('STAT:QUES:POW:COND?')) 155 pwrstat = int(r.ask('STAT:QUES:POW:COND?'))
154 if status != 0 or pwrstat != 0: 156 if status != 0 or pwrstat != 0:
155 print('Instrument warning, status %s power status %s' % 157 # Dump a screenshot for debugging
156 (bin(status), bin(pwrstat))) 158 screenshot(r, '%s-sweep-inst-warnings.png' % (ssprefix))
157 # Find the peak 159 raise Exception('Instrument warning, status %s power status %s' %
158 r.write('CALC:MARK1:MAX') 160 (bin(status), bin(pwrstat)))
159 cpwr = float(r.ask('CALC:MARK1:Y?')) 161 # Find the peak
160 if cpwr < cpwrlim: 162 r.write('CALC:MARK1 ON')
161 Exception('Power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim)) 163 r.write('CALC:MARK1:MAX')
162 cmeas = float(r.ask('CALC:MARK1:X?')) 164 cpwr = float(r.ask('CALC:MARK1:Y?'))
163 print('Found signal at %s with power %.1f dBm' % (sifmt(cmeas, dp = 6), cpwr)) 165 if cpwr < cpwrlim:
164 166 # Dump a screenshot for debugging
167 screenshot(r, '%s-sweep-no-power.png' % (ssprefix))
168 raise Exception('Power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim))
169 else:
170 found = True
171 cmeas = float(r.ask('CALC:MARK1:X?'))
172 print('Found signal at %s with power %.1f dBm' % (misc.sifmt(cmeas, dp = 6), cpwr))
173
174 if not found:
175 print('Unable to find signal')
165 # Centre peak 176 # Centre peak
166 r.write('CALC:MARK1:FUNC:CENT') 177 r.write('CALC:MARK1:FUNC:CENT')
167 178
168 # Set number of sweeps 179 # Set number of sweeps
169 r.write('SWE:COUN %d' % (sweeps)) 180 r.write('SWE:COUN %d' % (sweeps))
173 r.write('CALC:DELT1:FUNC:PNO') 184 r.write('CALC:DELT1:FUNC:PNO')
174 185
175 for idx, m in enumerate(measurements): 186 for idx, m in enumerate(measurements):
176 # Setup measurement 187 # Setup measurement
177 setupsweep(r, m['rbw'], m['vbw'], span = m['span']) 188 setupsweep(r, m['rbw'], m['vbw'], span = m['span'])
178
179 # Do the sweep 189 # Do the sweep
180 dosweep(r, sweeps) 190 dosweep(r, sweeps)
181 191
182 # Set offset of phase noise measurement 192 # Set offset of phase noise measurement
183 r.write('CALC:MARK1:MAX') 193 r.write('CALC:MARK1:MAX')
184 r.write('CALC:DELT1:MOD REL') 194 r.write('CALC:DELT1:MOD REL')
185 r.write('CALC:DELT1:X %f Hz' % (m['offset'])) 195 r.write('CALC:DELT1:X %f Hz' % (m['offset']))
186 meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?')) 196 meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?'))
187 print('Offset %7s %7.2f dBc/Hz' % (sifmt(m['offset'], dp = 0), meas)) 197 print('Offset %7s %7.2f dBc/Hz' % (misc.sifmt(m['offset'], dp = 0), meas))
188 198
189 # Take screen shot and save it 199 # Take screen shot and save it
190 if ssprefix is not None: 200 if ssprefix is not None:
191 screenshot(r, '%s-%d_%s_offset.png' % ( 201 screenshot(r, '%s-%d_%s_offset.png' % (
192 ssprefix, idx, sifmt(m['offset'], dp = 0, sp = ''))) 202 ssprefix, idx, misc.sifmt(m['offset'], dp = 0, sp = '')))
193 203
194 # Do a wide band scan 204 # Do a wide band scan
195 if ssprefix is not None: 205 if ssprefix is not None:
196 start = 1e6 206 start = 1e6
197 stop = 100e6 207 stop = 100e6
198 setupsweep(r, 1e3, 3e3, start = start, stop = stop) 208 setupsweep(r, 1e3, 3e3, start = start, stop = stop)
199 print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), 209 print('Scanning %s - %s (%.1f seconds)' % (misc.sifmt(start, dp = 0), misc.sifmt(stop, dp = 0),
200 float(r.ask('SWE:TIME?')))) 210 getsweeptime(r)))
201 dosweep(r, sweeps) 211 dosweep(r, sweeps)
202 # Show peaks 212 # Show peaks
203 r.write('CALC:MARK:AOFF') 213 r.write('CALC:MARK:AOFF')
204 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): 214 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))):
205 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) 215 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f))
208 218
209 # Look from just before signal to just after second harmonic 219 # Look from just before signal to just after second harmonic
210 start = nominal * 0.9 220 start = nominal * 0.9
211 stop = 2 * nominal + nominal * 0.1 221 stop = 2 * nominal + nominal * 0.1
212 setupsweep(r, 1e3, 3e3, start = start, stop = stop) 222 setupsweep(r, 1e3, 3e3, start = start, stop = stop)
213 print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), 223 print('Scanning %s - %s (%.1f seconds)' % (misc.sifmt(start, dp = 0), misc.sifmt(stop, dp = 0),
214 float(r.ask('SWE:TIME?')))) 224 getsweeptime(r)))
215 dosweep(r, sweeps) 225 dosweep(r, sweeps)
216 # Show peaks 226 # Show peaks
217 r.write('CALC:MARK:AOFF') 227 r.write('CALC:MARK:AOFF')
218 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): 228 for i, (f, p) in enumerate(zip(*findpeaks(r, 4))):
219 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) 229 r.write('CALC:MARK%d:X %f Hz' % (i + 1, f))