changeset 74:b6ebe05f250f

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 ca5a822c550a
children 576f112e0aba
files fsp7_phasenoise.py rs_fsp7_logmarker.py rsib.py scpisock.py
diffstat 4 files changed, 227 insertions(+), 12 deletions(-) [+]
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)))
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rs_fsp7_logmarker.py	Wed Sep 25 21:10:01 2024 +0930
@@ -0,0 +1,42 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2023
+#      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 rsib
+import time
+
+if __name__ == '__main__':
+    r = rsib.RSIBDevice('analyzer')
+
+    # ID instrument
+    r.write('*IDN?')
+    print("ID is " + r.read(5).decode('ascii'))
+
+    while True:
+        r.write('CALC1:MARK1:Y?')
+        print(r.read(5).decode('ascii'))
+        time.sleep(1)
+
--- a/rsib.py	Wed Sep 25 20:57:26 2024 +0930
+++ b/rsib.py	Wed Sep 25 21:10:01 2024 +0930
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# Copyright (c) 2009
+# Copyright (c) 2024
 #      Daniel O'Connor <darius@dons.net.au>.  All rights reserved.
 #
 # Redistribution and use in source and binary forms, with or without
@@ -25,6 +25,11 @@
 # SUCH DAMAGE.
 #
 
+# Helper functions to talk to Rohde & Schwarz test equipment over the
+# RSIB protocol.
+#
+# This is for older test equipment (tested on an FSP7 Windows NT 3.51..)
+#
 # Reverse engineered from the Linux library & example program at
 # http://epsrv.astro.umk.pl/~ep/irbene/rsib/library/RSIB-Linux.zip
 #
--- a/scpisock.py	Wed Sep 25 20:57:26 2024 +0930
+++ b/scpisock.py	Wed Sep 25 21:10:01 2024 +0930
@@ -36,9 +36,7 @@
 SCPI_PORT = 5025
 
 class SCPISockDevice(object):
-    def __init__(self, host, port = None):
-        if port == None:
-            port = SCPI_PORT
+    def __init__(self, host, port = SCPI_PORT):
         self.sock = socket.create_connection((host, port))
 
     def flush(self):
@@ -50,23 +48,23 @@
 
     def write(self, data):
         trail = ''
-        if data[-1] != '\n':
-            trail = '\n'
-            
+        if data[-1] != b'\n':
+            trail = b'\n'
+
         self.sock.send(data + trail)
 
     def read(self, timeout = None):
-        res = ''
+        res = b''
         if timeout == None:
             timeout = 0.1
-            
+
         while True:
             r, w, x = select.select([self.sock], [], [], timeout)
             if len(r) == 0:
                 break
             res = res + self.sock.recv(1024)
-            if res[-1] == '\n':
+            if res[-1] == b'\n':
                 break
 
-        return res.rstrip('\n')
-                        
+        return res.rstrip(b'\n')
+