# HG changeset patch # User Daniel O'Connor # Date 1727397355 -34200 # Node ID e2bb136bd2ed567a2c13606d39d8e43ea6ba0397 # Parent 576f112e0aba49854b1841eac2f1409a71cb39e6 Add script to use FSP7 to perform phasenoise measurements. Produces a list of measurements as well as optionally saving a set of (PNG) screenshots as well as wider sweeps. diff -r 576f112e0aba -r e2bb136bd2ed fsp7_phasenoise.py --- a/fsp7_phasenoise.py Fri Sep 27 09:27:33 2024 +0930 +++ b/fsp7_phasenoise.py Fri Sep 27 10:05:55 2024 +0930 @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright (c) 2014 +# Copyright (c) 2024 # Daniel O'Connor . All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -28,18 +28,45 @@ import argparse import math import numpy +import os +import PIL import rsib import scipy import scpi import sys +import time -def setup2(r, centre, span, rbw, vbw): - r.write('SENSE1:FREQ:CENT %f Hz' % (centre)) - r.write('SENSE1:FREQ:SPAN %f Hz' % (span)) - r.write('SENS1:BAND:RES %f Hz' % (rbw)) - r.write('SENS1:BAND:VID %f Hz' % (vbw)) +def sifmt(_v, dp = 3, unit = 'Hz', sp = ' '): + '''Format a number using SI prefixes''' + si_prefixes = ('T', 'G', 'M', 'k', '', 'm', 'ยต', 'n', 'p') + scale = 10 ** 12 + v = abs(_v) + if v == 0: + sip = "" + scale = 0 + for i, sip in enumerate(si_prefixes): + if v >= scale: + break + scale /= 1e3 + return ('%.' + str(dp) + 'f%s%s%s') % (_v / scale, sp, si_prefixes[i], unit) + +def setupsweep(r, rbw, vbw, centre = None, span = None, start = None, stop = None): + '''Helper function to set various sweep parameters''' + if centre is not None: + r.write('SENSE1:FREQ:CENT %f Hz' % (centre)) + if span is not None: + r.write('SENSE1:FREQ:SPAN %f Hz' % (span)) + if start is not None: + r.write('SENSE1:FREQ:START %f Hz' % (start)) + if stop is not None: + r.write('SENSE1:FREQ:STOP %f Hz' % (stop)) + if rbw is not None: + r.write('SENS1:BAND:RES %f Hz' % (rbw)) + if vbw is not None: + r.write('SENS1:BAND:VID %f Hz' % (vbw)) def dosweep(r, sweeps): + '''Helper function to trigger a sweep and wait for it to finish''' swt = float(r.ask('SWE:TIME?')) tout = swt * 5 * sweeps if tout < 1: @@ -53,16 +80,35 @@ opc = int(r.ask('*OPC?', timeout = tout)) assert(opc == 1) +def findpeaks(r, maxpeak = 5, minpwr = None): + '''Ask instrument to find maxpeaks peaks and return a list of frequencies and powers''' + peaks_f = [] + peaks_pwr = [] + r.write('CALC:MARK1:MAX') + for i in range(maxpeak): + frq = float(r.ask('CALC:MARK1:X?')) + pwr = float(r.ask('CALC:MARK1:Y?')) + if minpwr is not None and pwr < minpwr: + break + if i > 0: + if frq == peaks_f[i - 1]: + break + peaks_f.append(frq) + peaks_pwr.append(pwr) + r.write('CALC:MARK1:MAX:NEXT') -def setup(r, carrier, sweeps, rbw, vbw, ofs, atten): + return peaks_f, peaks_pwr + +def phasenoise(r, nominal, sweeps, atten, rlev, yscale, ssprefix): + '''Main function to find a signal, do some phase noise measurements and wideband sweeps''' cpwrlim = -30 measurements = ( - { 'offset' : 10, 'rbw' : 10, 'vbw' : 10 }, - { 'offset' : 100, 'rbw' : 10, 'vbw' : 10 }, - { 'offset' : 1e3, 'rbw' : 300, 'vbw' : 300 }, - { 'offset' : 10e3, 'rbw' : 300, 'vbw' : 300 }, - { 'offset' : 100e3, 'rbw' : 300, 'vbw' : 300 }, - { 'offset' : 1e6, 'rbw' : 1e3, 'vbw' : 3e3 }, + { 'offset' : 10, 'span' : 100, 'rbw' : 10, 'vbw' : 10 }, + { 'offset' : 100, 'span' : 250, 'rbw' : 10, 'vbw' : 10 }, + { 'offset' : 1e3, 'span' : 2.5e3, 'rbw' : 10, 'vbw' : 30 }, + { 'offset' : 10e3, 'span' : 25e3, 'rbw' : 100, 'vbw' : 300 }, + { 'offset' : 100e3, 'span' : 250e3, 'rbw' : 100, 'vbw' : 300 }, + { 'offset' : 1e6, 'span' : 2.1e6, 'rbw' : 1e3, 'vbw' : 3e3 }, ) # Reset to defaults r.write('*RST') @@ -74,17 +120,30 @@ r.write('SYST:DISP:UPD ON') # Set attenuation - r.write('INP:ATT %f' % atten) + if atten is None: + r.write('INP:ATT AUTO') + else: + r.write('INP:ATT %s' % atten) + + # Set Y scale + if yscale is not None: + r.write('DISP:TRAC:Y %s dB' % (yscale)) + + # Set reference level + if rlev is None: + r.write('DISP:WIND:TRAC:Y:RLEV AUTO') + else: + r.write('DISP:WIND:TRAC:Y:RLEV %f' % (rlev)) # - # Look for carrier + # Look for signal # - print('Measuring carrier') # Set frequency range etc - setup2(r, carrier, 1e3, 300, 300) + setupsweep(r, 10, 30, centre = nominal, span = 1e3) # Switch marker 1 on in screen A r.write('CALC:MARK1 ON') + print('Looking for signal (%.1f seconds)' % (float(r.ask('SWE:TIME?')))) # Do the sweep dosweep(r, 1) @@ -93,78 +152,113 @@ status = int(r.ask('STAT:QUES:COND?')) pwrstat = int(r.ask('STAT:QUES:POW:COND?')) if status != 0 or pwrstat != 0: - raise Exception('Instrument warning, status %s power status %s' % - (bin(status), bin(pwrstat))) - # Look for the carrier + print('Instrument warning, status %s power status %s' % + (bin(status), bin(pwrstat))) + # Find the peak r.write('CALC:MARK1:MAX') cpwr = float(r.ask('CALC:MARK1:Y?')) if cpwr < cpwrlim: - raise Exception('Carrier power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim)) + Exception('Power too low / not found: %.1f dBm vs %.1f dBm' % (cpwr, cpwrlim)) cmeas = float(r.ask('CALC:MARK1:X?')) - print('Found carrier at %.7f MHz with power %.1f dBm' % (cmeas / 1e6, cpwr)) + print('Found signal at %s with power %.1f dBm' % (sifmt(cmeas, dp = 6), cpwr)) - # Turn averaging on - r.write('AVER:STAT ON') + # Centre peak + r.write('CALC:MARK1:FUNC:CENT') # Set number of sweeps r.write('SWE:COUN %d' % (sweeps)) # Enable phase noise measurement + r.write('CALC:MARK:AOFF') r.write('CALC:DELT1:FUNC:PNO') for idx, m in enumerate(measurements): # Setup measurement - setup2(r, carrier, m['offset'] * 2.1, m['rbw'], m['vbw']) + setupsweep(r, m['rbw'], m['vbw'], span = m['span']) # Do the sweep dosweep(r, sweeps) # Set offset of phase noise measurement - r.write('CALC:DELT2:FUNC:FIX:RPO:X %f Hz' % (m['offset'])) + r.write('CALC:MARK1:MAX') + r.write('CALC:DELT1:MOD REL') + r.write('CALC:DELT1:X %f Hz' % (m['offset'])) meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?')) - print('Offset %.0fHz %.2f dBc/Hz' % (m['offset'], meas)) - return + print('Offset %7s %7.2f dBc/Hz' % (sifmt(m['offset'], dp = 0), meas)) + + # Take screen shot and save it + if ssprefix is not None: + screenshot(r, '%s-%d_%s_offset.png' % ( + ssprefix, idx, sifmt(m['offset'], dp = 0, sp = ''))) -def getphnoise(r): - # Trigger the sweep - r.write('INIT;*WAI') + # Do a wide band scan + if ssprefix is not None: + start = 1e6 + stop = 100e6 + setupsweep(r, 1e3, 3e3, start = start, stop = stop) + print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), + float(r.ask('SWE:TIME?')))) + dosweep(r, sweeps) + # Show peaks + r.write('CALC:MARK:AOFF') + for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): + r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) + screenshot(r, '%s-sweep-%.1f-%.1fMHz.png' % ( + ssprefix, start / 1e6, stop / 1e6)) - # Wait for it to be done - opc = int(r.ask('*OPC?', timeout = 1000)) - #print 'OPC - %d' % (opc) - assert(opc == 1) + # Look from just before signal to just after second harmonic + start = nominal * 0.9 + stop = 2 * nominal + nominal * 0.1 + setupsweep(r, 1e3, 3e3, start = start, stop = stop) + print('Scanning %s - %s (%.1f seconds)' % (sifmt(start, dp = 0), sifmt(stop, dp = 0), + float(r.ask('SWE:TIME?')))) + dosweep(r, sweeps) + # Show peaks + r.write('CALC:MARK:AOFF') + for i, (f, p) in enumerate(zip(*findpeaks(r, 4))): + r.write('CALC:MARK%d:X %f Hz' % (i + 1, f)) + screenshot(r, '%s-sweep-%.1f-%.1fMHz.png' % ( + ssprefix, start / 1e6, stop / 1e6)) - # Set data format - r.write('FORM:DATA ASC') +def screenshot(r, ssname, sapath = '\'c:\\temp\\pntmp.bmp\''): + # Setup & take screen shot + r.write('HCOPY:DEV:LANG1 BMP') + r.write('HCOPY:DEST1 MMEM') + r.write('HCOPY:CMAP:DEF2') + r.write('HCOPY:ITEM:ALL') + r.write('MMEM:NAME %s' % (sapath,)) + r.write('HCOPY:IMM1') + time.sleep(5) - # Read phase noise value - data = r.ask('CALC:MARK2:FUNC:NOIS:RES?') - #print 'Data - ' + data + # Grab it + r.write('MMEM:DATA? %s' % (sapath,)) + bmpdat = scpi.getbin(r.read()) + bmpname = ssname[0:-4] + '.bmp' - return float(data) + # Convert to whatever format + open(bmpname, 'wb').write(bmpdat) + img = PIL.Image.open(bmpname) + img.save(ssname) + os.unlink(bmpname) + r.write('MMEM:DEL %s' % (sapath,)) if __name__ == '__main__': parser = argparse.ArgumentParser(description = 'Configures a Rhode Schwartz FSP7 spectrum analyser to do a phase noise measurement') - parser.add_option('-s', '--span', dest = 'span', default = 1e6, help = 'Span frequency in Hz (default: %default)', type = float) - parser.add_option('-w', '--sweeps', dest = 'sweeps', default = 20, help = 'Number of sweeps (default: %default)', type = int) - parser.add_option('-r', '--rbw', dest = 'rbw', default = 1000, help = 'Resolution bandwidth in Hz (default: %default)', type = float) - parser.add_option('-v', '--vbw', dest = 'vbw', default = 3000, help = 'Video bandwidth in Hz (default: %default)', type = float) - parser.add_option('address', '--address', dest = 'address', help = 'Address of analyser', type = str) - parser.add_option('centre', '--centre', dest = 'centreq', help = 'Centre frequency of measurement', type = float) - parser.add_option('offset', '--offset', dest = 'offset', help = 'Offset frequency to take measurement at', type = float) + parser.add_argument('-a', '--atten', default = None, help = 'Attenuation level (default: autoamatic)') + parser.add_argument('-r', '--rlevel', default = 10, help = 'Reference level (default: %default dBm)', type = float) + parser.add_argument('-s', '--ssprefix', default = None, help = 'Path name to save screenshots to (default: none)', type = str) + parser.add_argument('-w', '--sweeps', default = 1, help = 'Number of sweeps (default: %default)', type = int) + parser.add_argument('-y', '--yscale', default = 120, help = 'Y-scale extent (default: %default dB)', type = float) + parser.add_argument('address', help = 'Address of analyser', type = str) + parser.add_argument('nominal', help = 'Nominal frequency of measurement (Hz)', type = float) - (options, args) = parser.parse_args() + args = parser.parse_args() # Connect to the analyser - r = rsib.RSIBDevice(addr) + r = rsib.RSIBDevice(args.address) # ID instrument - print('ID is', r.ask('*IDN?').decode('ascii')) - - # Setup parameters - setup(r, options.centre, options.span, options.sweeps, options.rbw, options.vbw, options.offset) + print('Device ID is', r.ask('*IDN?').decode('ascii')) - while True: - print('Centre: %.1f Mhz, Span %.1f Mhz, RBW %.1f kHz, %d sweeps, Offset %.1fkHz: %.2f dBc/Hz' % ( - options.centre / 1e6, options.span / 1e6, options.rbw / 1e3, options.sweeps, - options.offset / 1e3, getphnoise(r))) + # Do measurements + phasenoise(r, args.nominal, args.sweeps, args.atten, args.rlevel, args.yscale, args.ssprefix)