Tektronix Instruments
Control Tektronix oscilloscopes, signal generators, and analyzers using PyVISA with commands, examples, and troubleshooting.
Tektronix instrument control with PyVISA through examples, SCPI commands, and automation techniques.
Supported Tektronix Instruments
Oscilloscopes
- TDS/TBS Series: TDS2000, TBS1000, TBS2000 series
- MSO/DPO Series: MSO4000, DPO4000, MSO5000, DPO5000 series
- MDO Series: MDO3000, MDO4000 mixed-domain oscilloscopes
- RSA Series: Real-time spectrum analyzers with oscilloscope features
Signal Generators
- AFG Series: AFG3000, AFG31000 arbitrary function generators
- AWG Series: AWG5000, AWG7000 arbitrary waveform generators
Analyzers
- RSA Series: RSA5000, RSA6000 real-time spectrum analyzers
- MSO Series: Mixed-signal oscilloscopes with protocol analysis
Basic Connection and Setup
USB and Ethernet Connection
import pyvisa
import numpy as np
import matplotlib.pyplot as plt
import time
class TektronixInstrument:
"""Base class for Tektronix instruments"""
def __init__(self, resource_string):
self.rm = pyvisa.ResourceManager()
self.instrument = self.rm.open_resource(resource_string)
# Tektronix-specific timeout (often need longer)
self.instrument.timeout = 10000 # 10 seconds
# Verify connection
try:
self.idn = self.instrument.query('*IDN?')
print(f"Connected to: {self.idn.strip()}")
# Common Tektronix initialization
self.instrument.write('*RST') # Reset
self.instrument.write('*CLS') # Clear status
except Exception as e:
print(f"Connection failed: {e}")
raise
def check_errors(self):
"""Check for SCPI errors (Tektronix style)"""
try:
errors = []
while True:
error = self.instrument.query('SYST:ERR?')
if '0,"No error"' in error:
break
errors.append(error.strip())
return errors
except:
return [] # Some older Tektronix instruments don't support SYST:ERR
def close(self):
"""Close instrument connection"""
self.instrument.close()
self.rm.close()
# Connection examples
# USB: 'USB0::0x0699::0x0363::C102912::INSTR'
# Ethernet: 'TCPIP0::192.168.1.100::inst0::INSTR'
# GPIB: 'GPIB0::1::INSTR'
TDS/TBS/MSO/DPO Oscilloscope Control
Advanced Waveform Acquisition
class TektronixOscilloscope(TektronixInstrument):
"""Tektronix oscilloscope controller"""
def __init__(self, resource_string):
super().__init__(resource_string)
# Set up for optimal data transfer
self.instrument.write('DAT:ENC RIB') # Binary encoding
self.instrument.write('DAT:WID 2') # 16-bit data
self.instrument.write('VERB OFF') # Terse responses
def configure_acquisition(self, channels=[1], time_scale=1e-3,
voltage_scales=None, sample_rate=None):
"""Configure oscilloscope for measurement"""
# Set channels
for i in range(1, 5): # Channels 1-4
if i in channels:
self.instrument.write(f'SEL:CH{i} ON')
if voltage_scales and len(voltage_scales) >= i:
self.instrument.write(f'CH{i}:SCA {voltage_scales[i-1]}')
else:
self.instrument.write(f'SEL:CH{i} OFF')
# Set horizontal scale
self.instrument.write(f'HOR:SCA {time_scale}')
# Set sample rate if specified
if sample_rate:
self.instrument.write(f'HOR:SAM {sample_rate}')
# Acquisition mode
self.instrument.write('ACQ:MODE SAM') # Sample mode
self.instrument.write('ACQ:STOPA SEQ') # Single sequence
def setup_trigger(self, channel=1, level=0.0, edge='RISE'):
"""Configure trigger"""
self.instrument.write('TRIG:MAI:TYP EDGE') # Edge trigger
self.instrument.write(f'TRIG:MAI:EDGE:SOU CH{channel}')
self.instrument.write(f'TRIG:MAI:LEV {level}')
self.instrument.write(f'TRIG:MAI:EDGE:SLO {edge}')
def acquire_waveform(self, channel=1, timeout=30):
"""Acquire single waveform"""
# Select data source
self.instrument.write(f'DAT:SOU CH{channel}')
# Single acquisition
self.instrument.write('ACQ:STATE RUN')
self.instrument.write('ACQ:STOPA SEQ')
# Wait for trigger
start_time = time.time()
while time.time() - start_time < timeout:
state = self.instrument.query('ACQ:STATE?').strip()
if state == '0': # Acquisition stopped
break
time.sleep(0.1)
else:
raise TimeoutError("Acquisition timeout")
# Get waveform preamble
preamble = self.instrument.query('WFMP?').split(',')
# Parse preamble (Tektronix specific format)
y_scale = float(preamble[13]) # Vertical scale
y_offset = float(preamble[14]) # Vertical offset
y_position = float(preamble[15]) # Vertical position
x_scale = float(preamble[9]) # Horizontal scale
x_offset = float(preamble[10]) # Horizontal offset
# Get binary waveform data
raw_data = self.instrument.query_binary_values('CURV?', datatype='h')
# Convert to voltage values (Tektronix formula)
voltages = ((np.array(raw_data) - y_offset) * y_scale) + y_position
# Create time base
num_points = len(raw_data)
time_base = np.arange(num_points) * x_scale + x_offset
return time_base, voltages
def continuous_acquisition(self, channel=1, duration=10, callback=None):
"""Continuous waveform acquisition"""
self.instrument.write('ACQ:STOPA RUNST') # Run/stop mode
self.instrument.write('ACQ:STATE RUN')
waveforms = []
start_time = time.time()
while time.time() - start_time < duration:
try:
# Wait for new data
time.sleep(0.1)
# Check if acquisition is running
state = self.instrument.query('ACQ:STATE?').strip()
if state == '1': # Running
# Get latest waveform
time_data, voltage_data = self.acquire_waveform(channel)
waveform = {
'timestamp': time.time(),
'time': time_data,
'voltage': voltage_data
}
waveforms.append(waveform)
if callback:
callback(waveform)
except Exception as e:
print(f"Acquisition error: {e}")
self.instrument.write('ACQ:STATE STOP')
return waveforms
def measure_parameters(self, channel=1):
"""Get measurement parameters"""
measurements = {}
# Tektronix measurement commands
meas_commands = {
'frequency': f'MEASU:MEAS1:TYP FREQ;SOU CH{channel}',
'period': f'MEASU:MEAS2:TYP PERI;SOU CH{channel}',
'amplitude': f'MEASU:MEAS3:TYP AMP;SOU CH{channel}',
'mean': f'MEASU:MEAS4:TYP MEAN;SOU CH{channel}',
'rms': f'MEASU:MEAS5:TYP RMS;SOU CH{channel}',
'rise_time': f'MEASU:MEAS6:TYP RISE;SOU CH{channel}',
'fall_time': f'MEASU:MEAS7:TYP FALL;SOU CH{channel}',
'pulse_width': f'MEASU:MEAS8:TYP PWID;SOU CH{channel}'
}
for param, command in meas_commands.items():
try:
self.instrument.write(command)
time.sleep(0.1) # Allow measurement to complete
value = self.instrument.query(f'MEASU:MEAS{list(meas_commands.keys()).index(param)+1}:VAL?')
measurements[param] = float(value)
except:
measurements[param] = None
return measurements
def screenshot(self, filename="screenshot.png"):
"""Capture oscilloscope screenshot"""
# Set image format
self.instrument.write('SAV:IMAG:FILEF PNG')
# Get image data
image_data = self.instrument.query_binary_values('SAV:IMAG?', datatype='B')
# Save to file
with open(filename, 'wb') as f:
f.write(bytearray(image_data))
print(f"Screenshot saved as {filename}")
# Usage example
scope = TektronixOscilloscope('TCPIP0::192.168.1.100::inst0::INSTR')
# Configure for 1 MHz sine wave measurement
scope.configure_acquisition(channels=[1], time_scale=1e-6, voltage_scales=[0.5])
scope.setup_trigger(channel=1, level=0.1, edge='RISE')
# Single acquisition
time_data, voltage_data = scope.acquire_waveform(channel=1)
# Plot result
plt.figure(figsize=(12, 6))
plt.plot(time_data * 1e6, voltage_data) # Convert to microseconds
plt.xlabel('Time (μs)')
plt.ylabel('Voltage (V)')
plt.title('Tektronix Oscilloscope Waveform')
plt.grid(True)
plt.show()
# Get measurements
measurements = scope.measure_parameters(channel=1)
print("Measurements:", measurements)
scope.close()
Protocol Decoding and Analysis
class TektronixMSO(TektronixOscilloscope):
"""Tektronix Mixed Signal Oscilloscope with protocol analysis"""
def setup_digital_channels(self, channels, threshold=1.4):
"""Setup digital channels for MSO"""
for channel in channels:
if 0 <= channel <= 15: # Digital channels D0-D15
self.instrument.write(f'D{channel}:STATE ON')
self.instrument.write(f'D{channel}:THRES {threshold}')
def setup_i2c_decode(self, sda_channel='D0', scl_channel='D1',
clock_threshold=1.4, data_threshold=1.4):
"""Setup I2C protocol decoding"""
# Configure I2C bus
self.instrument.write('BUS:B1:TYPE I2C')
self.instrument.write(f'BUS:B1:I2C:SCLOCK:SOURCE {scl_channel}')
self.instrument.write(f'BUS:B1:I2C:SDATA:SOURCE {sda_channel}')
self.instrument.write(f'BUS:B1:I2C:SCLOCK:THRESHOLD {clock_threshold}')
self.instrument.write(f'BUS:B1:I2C:SDATA:THRESHOLD {data_threshold}')
# Enable bus display
self.instrument.write('BUS:B1:STATE ON')
self.instrument.write('BUS:B1:DISPLAY:TYPE BUS')
def setup_spi_decode(self, clock_channel='D0', data_channel='D1',
cs_channel='D2'):
"""Setup SPI protocol decoding"""
self.instrument.write('BUS:B1:TYPE SPI')
self.instrument.write(f'BUS:B1:SPI:CLOCK:SOURCE {clock_channel}')
self.instrument.write(f'BUS:B1:SPI:DATA:SOURCE {data_channel}')
self.instrument.write(f'BUS:B1:SPI:SELECT:SOURCE {cs_channel}')
# SPI settings
self.instrument.write('BUS:B1:SPI:CLOCK:POLARITY FALL')
self.instrument.write('BUS:B1:SPI:DATA:SIZE 8') # 8-bit data
self.instrument.write('BUS:B1:SPI:SELECT:POLARITY LOW')
self.instrument.write('BUS:B1:STATE ON')
def get_protocol_results(self):
"""Get decoded protocol results"""
# Search for protocol events
self.instrument.write('SEARCH:SEARCH1:TYPE BUS')
self.instrument.write('SEARCH:SEARCH1:SOURCE BUS1')
# Get search results
results = []
try:
# Get number of events found
count = int(self.instrument.query('SEARCH:SEARCH1:TOTAL?'))
for i in range(1, min(count + 1, 100)): # Limit to 100 events
# Get event details
self.instrument.write(f'SEARCH:SEARCH1:LIST:EVENT {i}')
time_pos = self.instrument.query('SEARCH:SEARCH1:LIST:TIME?')
data = self.instrument.query('SEARCH:SEARCH1:LIST:DATA?')
results.append({
'event': i,
'time': float(time_pos),
'data': data.strip()
})
except Exception as e:
print(f"Protocol decode error: {e}")
return results
# Usage for protocol analysis
mso = TektronixMSO('TCPIP0::192.168.1.100::inst0::INSTR')
# Setup digital channels
mso.setup_digital_channels([0, 1, 2], threshold=1.4)
# Setup I2C decoding
mso.setup_i2c_decode(sda_channel='D0', scl_channel='D1')
# Configure acquisition
mso.configure_acquisition(channels=[1], time_scale=1e-5)
# Trigger on I2C start condition
mso.instrument.write('TRIG:MAI:TYP BUS')
mso.instrument.write('TRIG:MAI:BUS:SOURCE B1')
mso.instrument.write('TRIG:MAI:BUS:I2C:COND START')
# Acquire and decode
time_data, voltage_data = mso.acquire_waveform(channel=1)
protocol_results = mso.get_protocol_results()
print(f"Found {len(protocol_results)} I2C events:")
for result in protocol_results[:10]: # Show first 10
print(f"Event {result['event']}: Time={result['time']:.6f}s, Data={result['data']}")
mso.close()
AFG/AWG Signal Generator Control
Arbitrary Function Generator Programming
class TektronixAFG(TektronixInstrument):
"""Tektronix Arbitrary Function Generator controller"""
def __init__(self, resource_string):
super().__init__(resource_string)
# AFG-specific initialization
self.instrument.write('*RST')
self.instrument.write('SOUR1:FUNC:SHAP SIN') # Default to sine wave
def set_sine_wave(self, frequency=1000, amplitude=1.0, offset=0.0, channel=1):
"""Generate sine wave"""
self.instrument.write(f'SOUR{channel}:FUNC:SHAP SIN')
self.instrument.write(f'SOUR{channel}:FREQ {frequency}')
self.instrument.write(f'SOUR{channel}:VOLT:AMPL {amplitude}')
self.instrument.write(f'SOUR{channel}:VOLT:OFFS {offset}')
def set_square_wave(self, frequency=1000, amplitude=1.0, duty_cycle=50, channel=1):
"""Generate square wave"""
self.instrument.write(f'SOUR{channel}:FUNC:SHAP SQU')
self.instrument.write(f'SOUR{channel}:FREQ {frequency}')
self.instrument.write(f'SOUR{channel}:VOLT:AMPL {amplitude}')
self.instrument.write(f'SOUR{channel}:PULS:DCYC {duty_cycle}')
def set_arbitrary_waveform(self, waveform_data, sample_rate=1e6, channel=1):
"""Upload arbitrary waveform"""
# Normalize data to -1 to +1 range
normalized_data = np.array(waveform_data)
normalized_data = normalized_data / np.max(np.abs(normalized_data))
# Convert to DAC values (Tektronix uses 14-bit DAC)
dac_data = (normalized_data * 8191).astype(np.int16)
# Create waveform name
wfm_name = f"ARB_WFM_{channel}"
# Upload waveform
self.instrument.write(f'SOUR{channel}:DATA:VOL:CLE')
# Send binary data
waveform_string = ','.join(map(str, dac_data))
self.instrument.write(f'SOUR{channel}:DATA:DAC EMEM,{waveform_string}')
# Set to arbitrary mode
self.instrument.write(f'SOUR{channel}:FUNC:SHAP USER')
self.instrument.write(f'SOUR{channel}:FUNC:USER EMEM')
# Set sample rate (affects playback frequency)
points = len(dac_data)
frequency = sample_rate / points
self.instrument.write(f'SOUR{channel}:FREQ {frequency}')
def set_modulation(self, mod_type='AM', mod_frequency=100, mod_depth=50, channel=1):
"""Configure modulation"""
if mod_type.upper() == 'AM':
self.instrument.write(f'SOUR{channel}:AM:STAT ON')
self.instrument.write(f'SOUR{channel}:AM:FREQ {mod_frequency}')
self.instrument.write(f'SOUR{channel}:AM:DEPT {mod_depth}')
elif mod_type.upper() == 'FM':
self.instrument.write(f'SOUR{channel}:FM:STAT ON')
self.instrument.write(f'SOUR{channel}:FM:FREQ {mod_frequency}')
self.instrument.write(f'SOUR{channel}:FM:DEV {mod_depth}') # Deviation in Hz
def set_burst_mode(self, burst_count=10, burst_period=0.01, channel=1):
"""Configure burst mode"""
self.instrument.write(f'SOUR{channel}:BURS:STAT ON')
self.instrument.write(f'SOUR{channel}:BURS:MODE TRIG') # Triggered burst
self.instrument.write(f'SOUR{channel}:BURS:NCYC {burst_count}')
self.instrument.write(f'SOUR{channel}:BURS:INT:PER {burst_period}')
def set_sweep(self, start_freq=1000, stop_freq=10000, sweep_time=1.0,
sweep_type='LIN', channel=1):
"""Configure frequency sweep"""
self.instrument.write(f'SOUR{channel}:SWE:STAT ON')
self.instrument.write(f'SOUR{channel}:FREQ:START {start_freq}')
self.instrument.write(f'SOUR{channel}:FREQ:STOP {stop_freq}')
self.instrument.write(f'SOUR{channel}:SWE:TIME {sweep_time}')
self.instrument.write(f'SOUR{channel}:SWE:SPAC {sweep_type}') # LIN or LOG
def enable_output(self, channel=1, enabled=True):
"""Enable/disable output"""
state = 'ON' if enabled else 'OFF'
self.instrument.write(f'OUTP{channel}:STAT {state}')
def sync_channels(self, master_channel=1, slave_channel=2, phase_offset=0):
"""Synchronize multiple channels"""
# Set slave channel to track master
self.instrument.write(f'SOUR{slave_channel}:FREQ:MODE CW')
self.instrument.write(f'SOUR{slave_channel}:FREQ:COUP ON')
self.instrument.write(f'SOUR{slave_channel}:PHAS:ADJ {phase_offset}')
# Usage examples
afg = TektronixAFG('USB0::0x0699::0x0346::C012345::INSTR')
# Generate 1 kHz sine wave
afg.set_sine_wave(frequency=1000, amplitude=2.0, offset=0.5, channel=1)
afg.enable_output(channel=1, enabled=True)
# Generate arbitrary waveform (sawtooth)
time_points = np.linspace(0, 1, 1000)
sawtooth = np.mod(time_points, 1.0) * 2 - 1 # -1 to +1 sawtooth
afg.set_arbitrary_waveform(sawtooth, sample_rate=1e6, channel=2)
afg.enable_output(channel=2, enabled=True)
# Add AM modulation
afg.set_modulation(mod_type='AM', mod_frequency=50, mod_depth=25, channel=1)
# Configure sweep
afg.set_sweep(start_freq=100, stop_freq=10000, sweep_time=5.0, channel=2)
afg.close()
RSA Spectrum Analyzer Control
Real-Time Spectrum Analysis
class TektronixRSA(TektronixInstrument):
"""Tektronix Real-time Spectrum Analyzer controller"""
def __init__(self, resource_string):
super().__init__(resource_string)
# RSA-specific settings
self.instrument.write('*RST')
self.instrument.write('INIT:CONT OFF') # Single sweep mode
def configure_spectrum(self, center_freq=1e9, span=100e6, rbw=1e6):
"""Configure basic spectrum analysis"""
self.instrument.write(f'FREQ:CENT {center_freq}')
self.instrument.write(f'FREQ:SPAN {span}')
self.instrument.write(f'BAND:RES {rbw}')
# Set detector mode
self.instrument.write('DET:TRAC:FUNC AVER') # Average detector
def acquire_spectrum(self, trace_name='TRACE1'):
"""Acquire spectrum trace"""
# Start sweep
self.instrument.write('INIT:IMM')
self.instrument.write('*OPC?')
self.instrument.read() # Wait for completion
# Get trace data
self.instrument.write(f'TRAC:DATA? {trace_name}')
spectrum_data = self.instrument.read_binary_values('f')
# Get frequency axis
center_freq = float(self.instrument.query('FREQ:CENT?'))
span = float(self.instrument.query('FREQ:SPAN?'))
num_points = len(spectrum_data)
frequencies = np.linspace(
center_freq - span/2,
center_freq + span/2,
num_points
)
return frequencies, spectrum_data
def real_time_analysis(self, duration=10, callback=None):
"""Continuous real-time spectrum analysis"""
self.instrument.write('INIT:CONT ON') # Continuous mode
spectra = []
start_time = time.time()
while time.time() - start_time < duration:
frequencies, spectrum = self.acquire_spectrum()
spectrum_data = {
'timestamp': time.time(),
'frequencies': frequencies,
'spectrum': spectrum
}
spectra.append(spectrum_data)
if callback:
callback(spectrum_data)
time.sleep(0.1) # 10 Hz update rate
self.instrument.write('INIT:CONT OFF')
return spectra
def peak_search(self, threshold=-50):
"""Find peaks in spectrum"""
frequencies, spectrum = self.acquire_spectrum()
# Find peaks above threshold
peak_indices = []
for i in range(1, len(spectrum) - 1):
if (spectrum[i] > threshold and
spectrum[i] > spectrum[i-1] and
spectrum[i] > spectrum[i+1]):
peak_indices.append(i)
peaks = []
for idx in peak_indices:
peaks.append({
'frequency': frequencies[idx],
'amplitude': spectrum[idx],
'index': idx
})
return peaks
def configure_iq_capture(self, center_freq=1e9, bandwidth=40e6):
"""Configure I/Q data capture"""
self.instrument.write('INST:SEL SA') # Spectrum analyzer mode
self.instrument.write(f'FREQ:CENT {center_freq}')
self.instrument.write(f'ACQ:BAND {bandwidth}')
# Set acquisition length
self.instrument.write('ACQ:POIN:AUTO ON')
def capture_iq_data(self):
"""Capture I/Q time domain data"""
# Start I/Q capture
self.instrument.write('INIT:IMM')
self.instrument.write('*OPC?')
self.instrument.read()
# Get I/Q data
i_data = self.instrument.query_binary_values('TRAC:IQ:DATA:I?', datatype='f')
q_data = self.instrument.query_binary_values('TRAC:IQ:DATA:Q?', datatype='f')
# Create complex array
iq_data = np.array(i_data) + 1j * np.array(q_data)
# Get time base
sample_rate = float(self.instrument.query('ACQ:BAND?'))
time_base = np.arange(len(iq_data)) / sample_rate
return time_base, iq_data
# Usage example
rsa = TektronixRSA('TCPIP0::192.168.1.100::inst0::INSTR')
# Configure for 1 GHz center, 100 MHz span
rsa.configure_spectrum(center_freq=1e9, span=100e6, rbw=1e6)
# Single spectrum acquisition
frequencies, spectrum = rsa.acquire_spectrum()
# Plot spectrum
plt.figure(figsize=(12, 6))
plt.plot(frequencies / 1e6, spectrum)
plt.xlabel('Frequency (MHz)')
plt.ylabel('Amplitude (dBm)')
plt.title('Tektronix RSA Spectrum')
plt.grid(True)
plt.show()
# Find peaks
peaks = rsa.peak_search(threshold=-40)
print(f"Found {len(peaks)} peaks:")
for peak in peaks:
print(f" {peak['frequency']/1e6:.2f} MHz: {peak['amplitude']:.1f} dBm")
rsa.close()
Advanced Automation Examples
Multi-Instrument Test System
class TektronixTestSystem:
"""Complete Tektronix test system controller"""
def __init__(self, scope_resource, afg_resource, rsa_resource=None):
self.scope = TektronixOscilloscope(scope_resource)
self.afg = TektronixAFG(afg_resource)
if rsa_resource:
self.rsa = TektronixRSA(rsa_resource)
else:
self.rsa = None
self.test_results = []
def frequency_response_test(self, frequencies, amplitude=1.0):
"""Automated frequency response measurement"""
results = []
for freq in frequencies:
print(f"Testing frequency: {freq/1000:.1f} kHz")
# Set AFG frequency
self.afg.set_sine_wave(frequency=freq, amplitude=amplitude, channel=1)
self.afg.enable_output(channel=1, enabled=True)
# Wait for settling
time.sleep(0.5)
# Configure scope for this frequency
time_scale = 2.0 / freq # 2 periods
self.scope.configure_acquisition(channels=[1], time_scale=time_scale)
self.scope.setup_trigger(channel=1, level=amplitude/4)
# Measure
time_data, voltage_data = self.scope.acquire_waveform(channel=1)
measurements = self.scope.measure_parameters(channel=1)
# Calculate response
if measurements['amplitude']:
response_db = 20 * np.log10(measurements['amplitude'] / amplitude)
else:
response_db = None
result = {
'frequency': freq,
'input_amplitude': amplitude,
'output_amplitude': measurements.get('amplitude'),
'response_db': response_db,
'phase': measurements.get('mean'), # Approximate phase
'measurements': measurements
}
results.append(result)
return results
def distortion_analysis(self, fundamental_freq=1000, amplitude=1.0):
"""Total Harmonic Distortion analysis"""
# Generate test signal
self.afg.set_sine_wave(frequency=fundamental_freq, amplitude=amplitude)
self.afg.enable_output(channel=1, enabled=True)
# Configure scope for high resolution
self.scope.configure_acquisition(channels=[1], time_scale=2.0/fundamental_freq)
# Acquire waveform
time_data, voltage_data = self.scope.acquire_waveform(channel=1)
# FFT analysis
sample_rate = 1.0 / (time_data[1] - time_data[0])
fft_data = np.fft.fft(voltage_data)
frequencies = np.fft.fftfreq(len(fft_data), 1/sample_rate)
# Find fundamental and harmonics
positive_freqs = frequencies[:len(frequencies)//2]
positive_fft = np.abs(fft_data[:len(fft_data)//2])
# Find fundamental peak
fund_idx = np.argmin(np.abs(positive_freqs - fundamental_freq))
fundamental_amplitude = positive_fft[fund_idx]
# Find harmonics
harmonics = []
for harmonic_num in range(2, 11): # 2nd to 10th harmonic
harmonic_freq = fundamental_freq * harmonic_num
if harmonic_freq < sample_rate / 2: # Within Nyquist limit
harm_idx = np.argmin(np.abs(positive_freqs - harmonic_freq))
harmonic_amplitude = positive_fft[harm_idx]
harmonics.append({
'harmonic': harmonic_num,
'frequency': harmonic_freq,
'amplitude': harmonic_amplitude,
'amplitude_db': 20 * np.log10(harmonic_amplitude / fundamental_amplitude)
})
# Calculate THD
harmonic_power = sum([h['amplitude']**2 for h in harmonics])
thd_percent = 100 * np.sqrt(harmonic_power) / fundamental_amplitude
return {
'fundamental_frequency': fundamental_freq,
'fundamental_amplitude': fundamental_amplitude,
'harmonics': harmonics,
'thd_percent': thd_percent,
'spectrum': {
'frequencies': positive_freqs,
'amplitudes': positive_fft
}
}
def close_all(self):
"""Close all instruments"""
self.scope.close()
self.afg.close()
if self.rsa:
self.rsa.close()
# Usage example
test_system = TektronixTestSystem(
scope_resource='TCPIP0::192.168.1.100::inst0::INSTR',
afg_resource='USB0::0x0699::0x0346::C012345::INSTR'
)
# Frequency response sweep
frequencies = np.logspace(2, 5, 50) # 100 Hz to 100 kHz
response_data = test_system.frequency_response_test(frequencies, amplitude=1.0)
# Plot frequency response
freqs = [r['frequency'] for r in response_data if r['response_db'] is not None]
responses = [r['response_db'] for r in response_data if r['response_db'] is not None]
plt.figure(figsize=(12, 6))
plt.semilogx(freqs, responses)
plt.xlabel('Frequency (Hz)')
plt.ylabel('Response (dB)')
plt.title('Frequency Response')
plt.grid(True)
plt.show()
# Distortion analysis
distortion_data = test_system.distortion_analysis(fundamental_freq=1000)
print(f"THD: {distortion_data['thd_percent']:.3f}%")
test_system.close_all()
Common Issues and Troubleshooting
Connection Problems
def diagnose_tektronix_connection(resource_string):
"""Diagnose connection issues with Tektronix instruments"""
print("Tektronix Connection Diagnostics")
print("=" * 40)
try:
# Try basic connection
rm = pyvisa.ResourceManager()
print(f"Resource Manager: {rm}")
# List available resources
resources = rm.list_resources()
print(f"Available resources: {resources}")
# Attempt connection
instrument = rm.open_resource(resource_string)
print(f"Connected to: {resource_string}")
# Test basic communication
idn = instrument.query('*IDN?')
print(f"Identification: {idn.strip()}")
# Test error queue
try:
error = instrument.query('SYST:ERR?')
print(f"Error status: {error.strip()}")
except:
print("Error: SYST:ERR not supported")
# Test specific Tektronix commands
try:
# Try oscilloscope command
state = instrument.query('ACQ:STATE?')
print(f"Acquisition state: {state.strip()}")
except:
print("Note: Not an oscilloscope or acquisition command failed")
try:
# Try AFG command
freq = instrument.query('SOUR1:FREQ?')
print(f"Source frequency: {freq.strip()}")
except:
print("Note: Not an AFG or source command failed")
instrument.close()
rm.close()
print("✅ Connection successful!")
except pyvisa.errors.VisaIOError as e:
print(f"❌ VISA Error: {e}")
print("\nTroubleshooting suggestions:")
print("1. Check instrument power and connections")
print("2. Verify IP address for Ethernet connections")
print("3. Install/update Tektronix VISA drivers")
print("4. Check firewall settings")
except Exception as e:
print(f"❌ Unexpected error: {e}")
# Run diagnostics
diagnose_tektronix_connection('TCPIP0::192.168.1.100::inst0::INSTR')
Next Steps
- Keysight instruments: Keysight Instruments
- Rohde & Schwarz: Rohde & Schwarz Instruments
- Performance optimization: Performance Guide
How is this guide?