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:
Initialize the RF signal generator.
Initialize the detector under test.
Take the detector voltage measurements.
Fit lines to the linear portions of the detector response for each of the input signal frequencies.
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.
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.
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 withsmhu_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 )