view fsp7_phasenoise.py @ 75:576f112e0aba

Fix read timeout handling so it works over imperfect links.
author Daniel O'Connor <doconnor@gsoft.com.au>
date Fri, 27 Sep 2024 09:27:33 +0930
parents b6ebe05f250f
children e2bb136bd2ed
line wrap: on
line source

#!/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)))