PyVISA
PyVISADocs

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

How is this guide?