diff fsp7_phasenoise.py @ 74:b6ebe05f250f default tip

Add some commentry about what it works with
author Daniel O'Connor <doconnor@gsoft.com.au>
date Wed, 25 Sep 2024 21:10:01 +0930
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/fsp7_phasenoise.py	Wed Sep 25 21:10:01 2024 +0930
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+
+# Copyright (c) 2014
+#      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.
+#
+
+import argparse
+import math
+import numpy
+import rsib
+import scipy
+import scpi
+import sys
+
+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 dosweep(r, sweeps):
+    swt = float(r.ask('SWE:TIME?'))
+    tout = swt * 5 * sweeps
+    if tout < 1:
+        tout = 1
+    #print('Sweep time', swt)
+
+    # Trigger the sweep
+    r.write('INIT;*WAI')
+
+    # Wait for it to be done
+    opc = int(r.ask('*OPC?', timeout = tout))
+    assert(opc == 1)
+
+
+def setup(r, carrier, sweeps, rbw, vbw, ofs, atten):
+    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 },
+    )
+    # 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 attenuation
+    r.write('INP:ATT %f' % atten)
+
+    #
+    # Look for carrier
+    #
+    print('Measuring carrier')
+    # Set frequency range etc
+    setup2(r, carrier, 1e3, 300, 300)
+
+    # Switch marker 1 on in screen A
+    r.write('CALC:MARK1 ON')
+
+    # Do the sweep
+    dosweep(r, 1)
+
+    # Check the instrument is happy
+    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
+    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))
+    cmeas = float(r.ask('CALC:MARK1:X?'))
+    print('Found carrier at %.7f MHz with power %.1f dBm' % (cmeas / 1e6, cpwr))
+
+    # Turn averaging on
+    r.write('AVER:STAT ON')
+
+    # Set number of sweeps
+    r.write('SWE:COUN %d' % (sweeps))
+
+    # Enable phase noise measurement
+    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'])
+
+        # Do the sweep
+        dosweep(r, sweeps)
+
+        # Set offset of phase noise measurement
+        r.write('CALC:DELT2:FUNC:FIX:RPO:X %f Hz' % (m['offset']))
+        meas = float(r.ask('CALC:DELT:FUNC:PNO:RES?'))
+        print('Offset %.0fHz %.2f dBc/Hz' % (m['offset'], meas))
+    return
+
+def getphnoise(r):
+    # Trigger the sweep
+    r.write('INIT;*WAI')
+
+    # Wait for it to be done
+    opc = int(r.ask('*OPC?', timeout = 1000))
+    #print 'OPC - %d' % (opc)
+    assert(opc == 1)
+
+    # Set data format
+    r.write('FORM:DATA ASC')
+
+    # Read phase noise value
+    data = r.ask('CALC:MARK2:FUNC:NOIS:RES?')
+    #print 'Data - ' + data
+
+    return float(data)
+
+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)
+
+    (options, args) = parser.parse_args()
+
+    # Connect to the analyser
+    r = rsib.RSIBDevice(addr)
+
+    # 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)
+
+    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)))