Open RF Prototyping

measure-cal-data.py

Initially, the script processes the command line and then invokes the main function passing the specified command arguments or their defaults.

<<imports>>

<<globals>>

<<main>>

if __name__ == '__main__':

    defaultDevice = '/dev/tty.usbmodem14101'
    defaultBaud = '1500000'
    defaultOutfile = 'pwrdetect-cal-data.json'

    parser = ArgumentParser(description=
      '''Calibrate power detector module.''')

    parser.add_argument("-d", "--device",
                      default=defaultDevice,
                      help="The serial device (default: {})".format(defaultDevice))
    parser.add_argument("-C", "--cspin", default='D0',
                        help="Chip select controller pin (default: {})".format('D0'))
    parser.add_argument("-G", "--gen", choices=['DSG815', 'SMHU58'],
                        default='SMHU58',
                        help="""The signal generator in use for
                        the calibration. Default: {}""".format('SMHU58'))
    parser.add_argument("-O", "--outfile",
                        default=defaultOutfile,
                        help="""Output file for measured calibration data.
                        Default: {}""".format(defaultOutfile))

    args = parser.parse_args()

    main(args.device, args.cspin, args.gen, args.outfile)

The main Function: <<main>>

The main function proceeds in five stages:

  1. Initialize the RF signal generator.

  2. Initialize the detector under test.

  3. Take the detector voltage measurements.

  4. Fit lines to the linear portions of the detector response for each of the input signal frequencies.

  5. Save measured data and associated linear fits to a file.

def main(serdev, cspin, gen, outfile):

    <<initialize-detector>>

    <<initialize-siggen>>

    <<take-measurements>>

    <<fit-measurements>>

    data_out = {
        "detector_type" : det.detector_type,
        "measured_data" : vout_vs_freq,
        "linear_fits" : linear_fits}
    with open(outfile, 'w') as fd:
        json.dump(data_out, fd)

The data is saved as a JSON formatted dictionary containing two entries for the measured data and linear fits. The format of the saved data is illustrated in the following code snippet:

import json

with open('measuredData/ltc5582/ltc5582-cal-data.json') as fd:
    d = json.load(fd)

# The measured voltages are lists keyed with the input signal frequency
print(d['measured_data'].keys())
dict_keys(['250.0', '500.0', '750.0', '1000', '1250', '1500', '1750', '2000', '2250', '2500', '2750', '3000', '3250', '3500', '3750', '4000', '4250'])

# Detector voltages are measured for each of the input signal power levels
print(d['measured_data']['1500'])
[533.75, 543.90625, 595.0, 677.8125, 799.6875, 949.6875, 1108.59375, 1265.46875, 1423.90625, 1577.96875, 1729.0625, 1881.71875, 2031.40625, 2181.09375, 2322.8125, 2452.03125, 2535.625, 2554.53125]

# The linear fits are lists keyed with the input signal frequency
print(d['linear_fits'].keys())
dict_keys(['250.0', '500.0', '750.0', '1000', '1250', '1500', '1750', '2000', '2250', '2500', '2750', '3000', '3250', '3500', '3750', '4000', '4250'])

# Linear fit data is saved in the following format:
#
# [slope, intercept, slope std. dev., intercept std. dev.]
print(d['linear_fits']['1500'])
[30.62313988442146, 2492.03869057159, 0.12726930370323475, 3.7914719693402366]

Initialize the signal generator: <<initialize-siggen>>

The script currently supports either the R&S SMHU and the Rigol DSG815. Depending on the which one is being used either GPIB or VISA is used for instrument communication and command. Note that the frequencies list is set to the frequency range appropriate for the selected signal generator.

if gen == 'DSG815':
    visa_rm = pyvisa.ResourceManager('@py')
    sig_gen = DSG815(visa_rm, DSG815_ID)
    sig_gen.initialize()
    frequencies = dsg815_frequencies[det.detector_type]
elif gen == 'SMHU58':
    gpib = GPIB()
    gpib.initialize()
    sig_gen = SMHU58(gpib, SMHU58_GPIBID)
    sig_gen.initialize()
    frequencies = smhu_frequencies[det.detector_type]
else:
    print("ERROR: Unknown signal generator: {}".format(gen))
    sys.exit(-1)

Initialize the detector: <<initialize-detector>>

A serial communications link is first established with the atmega USB control board. An instance of the rfblocks LogDetector is then created. Note that initializing the LogDetector det instance may raise a InvalidCalibrationDataError exception if there is no calibration data currently resident in the attiny45 EEPROM. However, the initialization must be carried out in order to read the power detector type and initialize the detector parameters.

try:
    ser = create_serial(serdev)
except OSError as error:
    print('No serial port ({})'.format(serdev))
    sys.exit(-1)

det = LogDetector(cspin)
write_cmd(ser, det.pin_config())
try:
    det.initialize(ser)
except InvalidCalibrationDataError:
    print("Detector has no valid calibration data.")

Take measurements: <<take-measurements>>

For each of the input power levels specified in pwr_levels measure raw detector voltages at each frequency specified in the frequencies list. The voltages reported by the detector under test are saved in measured_levels.

measured_levels = {}

for pwr in pwr_levels[det.detector_type]:

    print('{:6.1f}: '.format(pwr), end='', flush=True)
    sig_gen.level = pwr
    sig_gen.output = True
    sleep(2.0)

    for freq in frequencies:

        sig_gen.freq = freq
        sleep(1.0)
        measured_pwr = det.vout(ser, avg=16)
        print(' {:5.2f}'.format(measured_pwr), end='', flush=True)
        try:
            measured_levels[freq].append(measured_pwr)
        except KeyError:
            measured_levels[freq] = [measured_pwr]

    print()

sig_gen.level = -40.0
sig_gen.reset()

if gen == 'DSG815':
    visa_rm.close()
else:
    gpib.close()

Fit measurements: <<fit-measurements>>

Lines are fitted to the linear regions of the detector responses for each of the input signal frequencies. The detector linear response regions will vary with the input signal frequency. Based on the LTC5582 data sheet, Table 2 lists the linear response regions as a function of input signal frequencies.

Table 2 Detector linear response regions

Frequency Range (MHz)

Linear Response Region (dBm)

Index Slices

LTC5582

0 .. 2700

-45 .. -10

6:14

2700 .. 4300

-35 .. -10

8:14

LT5537

0 .. 600

-60 .. -5

4:16

The scipy optimize.curve_fit function is used to fit a linear function to the linear response region. The index slices shown in column 3 of Table 2 are the list slices which access the linear response regions of the input signal power and measured detector voltage lists (pwr_levels and levels respectively). The curve_fit function returns the slope and intercept of the best fit line together with the 2x2 array of covariances for the fit parameters. One standard deviation errors for the fit parameters are calculated from the trace of the covariances array.

def linear_func(x, a, b):
    return a*x + b

linear_fits = {}
vout_vs_freq = {}
for freq, levels in measured_levels.items():
    vout_vs_freq[freq] = levels

    if det.detector_type == LogDetector.LTC5582:
        # Linear regions:
        #  0    -> 2700 MHz:  -45 .. -10
        #  2700 -> 4300 MHz:  -35 .. -10
        if freq < 2750.0:
            vout = levels[6:14]
            pwr = pwr_levels[LogDetector.LTC5582][6:14]
        else:
            vout = levels[8:14]
            pwr = pwr_levels[LogDetector.LTC5582][8:14]
    elif det.detector_type == LogDetector.LT5537:
        vout = levels[4:16]
        pwr = pwr_levels[LogDetector.LT5537][4:16]
    else:
        raise RuntimeError(
            'No such detector type: {}'.format(det.detector_type))

    popt, pcov = curve_fit(linear_func, pwr, vout)
    perr = np.sqrt(np.diag(pcov))
    slope = popt[0]
    intercept = popt[1]
    linear_fits[freq] = [slope, intercept, perr[0], perr[1]]

Globals: <<globals>>

Since the different signal generator models have differing frequency capabilities the measured frequency steps will depend on the selected signal generator. These steps are stored in the frequencies list for later use.

pwr_levels

the test signal power levels to use.

smhu_frequencies

the test signal frequencies to use for the R&S SMHU58 signal generator

dsg815_frequencies

the test signal frequencies to with the Rigol DSG815 signal generator

frequencies

the actual test signal frequencies to use during the measurement process. The default signal generator is the SMHU58 and so the frequencies variable is initialized with smhu_frequencies.

pwr_levels = [
    [-75.0, -70.0, -65.0, -60.0, -55.0, -50.0, -45.0, -40.0, -35.0,
     -30.0, -25.0, -20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0],
    [-80.0, -75.0, -70.0, -65.0, -60.0, -55.0, -50.0, -45.0, -40.0,
     -35.0, -30.0, -25.0, -20.0, -15.0, -10.0, -5.0, 0.0, 5.0, 10.0],
]

smhu_frequencies = [
    [250.0, 500.0, 750.0, 1000, 1250, 1500, 1750, 2000, 2250,
     2500, 2750, 3000, 3250, 3500, 3750, 4000, 4250],
    [5.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0,
     400.0, 425.0, 450.0, 475.0, 500.0, 525.0, 550.0, 575.0, 600.0]
]

dsg815_frequencies = [
    [250.0, 500.0, 750.0, 1000, 1250, 1500],
    [5.0, 50.0, 100.0, 150.0, 200.0, 250.0, 300.0, 350.0,
     400.0, 425.0, 450.0, 475.0, 500.0, 525.0, 550.0, 575.0, 600.0]
]

frequencies = smhu_frequencies

Imports: <<imports>>

import sys
from time import sleep
from argparse import ArgumentParser
import json
import numpy as np
from scipy.optimize import curve_fit
import pyvisa
from spidriver import SPIDriver
from tam import (
    GPIB, SMHU58, SMHU58_GPIBID, DSG815, DSG815_ID
)
from rfblocks import (
    LogDetector, create_serial, write_cmd, query_cmd,
    InvalidCalibrationDataError
)