UNCLASSIFIED
TM-INST-028
TINYSA SPECTRUM ANALYZER CALIBRATION OVERVIEW
Construction, Theory, Calibration and Verification Procedures
Prepared by: Mervyn Martin, KO6NNH  •  Merced, California  •  26 May 2026
Amateur Radio / Electronics — Not for commercial calibration use

CHAPTER 1 — GENERAL INFORMATION

Gear Profile

  • Example models: TinySA Ultra, TinySA Basic
  • Frequency range: 100 kHz to 6 GHz (Ultra); 100 kHz to 960 MHz (Basic)
  • Connector type: SMA
  • Typical use: Spectrum analysis, signal hunting, filter checking

Purpose

This guide describes a homebrew, no-calibration-needed method to calibrate a TinySA spectrum analyzer using public reference signals and first-principles checks. It borrows techniques from:

  • ../gps_calibration.md
  • ../fm_broadcast_calibration.md
  • ../radio_standard_calibration.md
  • ../verification_procedures.md

What You Will Build

  • A simple reference load (homebrew 50 ohm)
  • A quarter-wave coax stub test fixture
  • Optional GPS 1PPS counter (if you want tighter frequency checks)

Expected Accuracy

  • Frequency axis: 0.1 to 1 ppm (with GPS or time standards)
  • SWR/impedance: limited by resistor tolerance (typically 1-2%)

CHAPTER 2 — THEORY OF OPERATION

Calibration Philosophy

We use absolute references (GPS 1PPS, WWV/CHU, FM stations) and known physics (speed of light, coax velocity factor, resonance) to validate the analyzer without relying on any pre-calibrated lab equipment.

Key Principles

  1. Frequency accuracy: Check displayed frequency against GPS/WWV/CHU/FM carriers.
  2. Impedance accuracy: Validate using a known load and known reactance from a stub or LC.
  3. Repeatability: Measurements should be stable over time and temperature.

Reference Sources

  • GPS 1PPS: Atomic time tick (best accuracy).
  • WWV/CHU: HF time standards (good accuracy).
  • FM broadcast: Convenient local reference (moderate accuracy).

CHAPTER 3 — MATERIALS AND CONSTRUCTION

Build a 50 ohm Load

  • Use four 200 ohm, 1% resistors in parallel.
  • Solder directly inside a BNC/SMA connector shell if possible.
  • Keep leads short to reduce inductance.

Build a Quarter-Wave Stub

  • Choose coax with known velocity factor.
  • Cut to calculated length (see Calculations).
  • Short the far end (center to shield).
  • Label the stub with its target frequency and VF.

Optional GPS 1PPS Interface

  • GPS module with 1PPS output.
  • LED + resistor for lock indication.
  • Optional ESP32/CYD counter (see optional code example).

CHAPTER 4 — ASSEMBLY PROCEDURES

Assembly Steps

  1. Build or verify the 50 ohm load.
  2. Build one or more quarter-wave stubs (pick key bands).
  3. Prepare short, known-good test leads.
  4. Warm up the analyzer (10-15 minutes) before calibration checks.

CHAPTER 5 — CALIBRATION PROCEDURE

Step-by-Step

  1. Warm up the analyzer for 10-15 minutes.
  2. Frequency check using WWV/CHU or FM station:
  3. Measure known carrier.
  4. Calculate ppm error.
  5. Apply correction if supported.
  6. Impedance check using the 50 ohm load:
  7. Confirm 45-55 ohm range.
  8. Record SWR.
  9. Reactive check using the shorted stub:
  10. Find resonance dip.
  11. Compare with calculated frequency.
  12. Document the offsets and repeatability.

CHAPTER 6 — TUNING AND ADJUSTMENT

Frequency Axis Check

  • Measure WWV/CHU or FM carrier frequency.
  • Compare to published frequency.
  • If your analyzer supports frequency offset adjustment, apply correction.

Impedance/SWR Check

  • Connect the 50 ohm load.
  • The analyzer should read close to 50 ohm and low SWR.
  • Connect the shorted stub and locate the resonance dip; compare to expected.

Trim and Iterate

  • If stub resonance is off, trim length in small increments.
  • Re-check until resonance aligns within expected tolerance.

CHAPTER 7 — VERIFICATION

Verification Checklist

  • Re-measure the reference carrier after calibration.
  • Measure a second independent reference (FM vs WWV/CHU).
  • Confirm measurements are stable across 3-5 repetitions.

Acceptance Targets

  • Frequency accuracy: within 0.1-1 ppm of reference.
  • Load accuracy: within 1-2% of 50 ohm.

APPENDIX A — CALCULATIONS AND FORMULAS

Quarter-Wave Coax Stub

Use a shorted coax stub to create a known resonance:

L = (c / (4 * f)) * VF
                

Where: - L = stub length (meters) - c = 299,792,458 m/s - f = frequency (Hz) - VF = velocity factor of coax (e.g., 0.66 solid PE, 0.78 foam)

Example

Target frequency: 14.200 MHz

L = (299,792,458 / (4 * 14,200,000)) * 0.66
                L = 3.49 m
                

Cut slightly long, then trim while watching the analyzer until resonance hits target.

50 ohm Load (Homebrew)

Parallel resistor network:

R_total = 1 / (1/R1 + 1/R2 + ... + 1/Rn)
                

Example

Four 200 ohm resistors in parallel:

R_total = 1 / (4/200) = 50 ohms
                

Use 1% or 0.1% resistors if possible.

APPENDIX B — EXAMPLE RESULTS

Example Log

Date: 2026-01-15
                Gear: Example Analyzer
                
                Reference: WWV 15 MHz
                Measured: 15,000,012 Hz
                Error: +0.8 ppm
                
                Load Test: 50 ohm load
                Measured: 50.9 ohm
                SWR: 1.02
                
                Stub Test (14.200 MHz target)
                Measured dip: 14.198 MHz
                Error: -0.14%
                

APPENDIX C — VERIFICATION PROCEDURES

Overview

After calibrating your TinySA using GPS, time standard broadcasts, or FM stations, you need to verify the calibration worked and characterize long-term performance.

This document provides: 1. Immediate verification procedures 2. Cross-checking against multiple references 3. Long-term stability testing 4. Temperature characterization 5. Uncertainty analysis


Immediate Verification (Post-Calibration)

Quick Check (5 minutes)

Purpose: Confirm calibration was applied correctly

Procedure:

  1. Re-measure calibration source:
  2. GPS 1PPS → Should read 1.000000 Hz (±0.001 Hz)
  3. CHU 7.850 MHz → Should read 7,850,000 Hz (±10 Hz)
  4. FM 100.1 MHz → Should read 100,100,000 Hz (±100 Hz)

  5. Check for improvement: ``` BEFORE calibration: CHU measured: 7,850,152 Hz Error: +152 Hz (+19.4 ppm)

AFTER calibration: CHU measured: 7,850,003 Hz Error: +3 Hz (+0.38 ppm) ```

  1. Verify correction was saved:
  2. Power cycle TinySA
  3. Re-measure
  4. Should still be accurate

Success Criteria: - ✓ Error reduced by >90% - ✓ Final error < 1 ppm (< 30 Hz @ 30MHz) - ✓ Calibration survives power cycle


Cross-Reference Validation (30 minutes)

Multiple Independent Sources

Purpose: Confirm calibration against different references

Procedure:

  1. GPS vs. WWV/CHU: ``` Method A (GPS):
  2. Measured 30MHz error: +5.2 ppm
  3. Applied correction: -5.2 ppm

Method B (CHU 7.850 MHz): - Measure CHU frequency - Calculate ppm error - Should read ±0.5 ppm of GPS result ```

  1. GPS vs. FM Broadcast: ``` After GPS calibration, measure FM station:
  2. Should read exact frequency (±1 ppm)
  3. Example: 100.1 MHz → 100,100,000 Hz ±100 Hz ```

  4. Multiple FM Stations: Measure 3-5 different FM stations All should read exact frequency If one is off, that station is poorly calibrated

Acceptance Criteria

All methods should agree within: - GPS vs. Time Standard: ±0.1-0.5 ppm - GPS vs. FM Broadcast: ±1-2 ppm - FM vs. FM: ±0.5 ppm (same market)

Example Results (Good):

Method Error (ppm) Agreement
GPS +5.23 Reference
CHU +5.19 ±0.04 ppm ✓
FM 1 +5.21 ±0.02 ppm ✓
FM 2 +5.18 ±0.05 ppm ✓

Example Results (Problem):

Method Error (ppm) Agreement
GPS +5.23 Reference
CHU +5.22 ±0.01 ppm ✓
FM 1 +8.45 ±3.22 ppm ✗ Bad station
FM 2 +5.19 ±0.04 ppm ✓

Short-Term Stability Test (1 hour)

Allan Deviation (Simplified)

Purpose: Measure frequency stability over short time scales

Equipment Needed: - GPS frequency counter (from construction guide) - TinySA 30MHz signal - Computer for logging

Procedure:

  1. Set up continuous measurement:
  2. GPS 1PPS gates TinySA 30MHz
  3. Measure every second
  4. Log to file

  5. Run for 1 hour (3600 measurements)

  6. Calculate statistics: ```python import numpy as np import pandas as pd

# Load data data = pd.read_csv('frequency_log.csv') freq = data['Frequency_Hz'].values

# Calculate Allan deviation for τ = 1s expected = 30000000 y = (freq - expected) / expected # Fractional frequency

allan_1s = np.std(y) print(f"Allan deviation (1s): {allan_1s*1e6:.3f} ppm")

# Calculate for τ = 10s (average groups of 10) y_10s = [] for i in range(0, len(y)-10, 10): y_10s.append(np.mean(y[i:i+10])) allan_10s = np.std(y_10s) / np.sqrt(2) print(f"Allan deviation (10s): {allan_10s*1e6:.3f} ppm") ```

Typical Results

Good TCXO:

Allan deviation (1s):  0.1-0.5 ppm
                Allan deviation (10s): 0.01-0.1 ppm
                Allan deviation (100s): 0.005-0.05 ppm
                

Poor crystal:

Allan deviation (1s):  1-5 ppm
                Allan deviation (10s): 0.5-2 ppm
                

Interpretation: - Lower = better - Should improve with longer averaging - If increases with time → drift problem - If flat → hitting measurement noise floor


Long-Term Stability Test (24 hours)

24-Hour Frequency Log

Purpose: Characterize drift, temperature effects, aging

Setup:

  1. Stable environment:
  2. Indoors, away from windows
  3. Minimize temperature swings
  4. Avoid moving equipment

  5. Continuous logging:

  6. Measure every 1-5 minutes
  7. Log frequency, temperature
  8. Save to CSV file

  9. Run for 24+ hours

Python Logging Script

import serial
                import time
                from datetime import datetime
                
                ser = serial.Serial('/dev/ttyUSB0', 115200, timeout=1)
                time.sleep(2)
                
                log_file = open('24hr_stability.csv', 'w')
                log_file.write('Timestamp,Frequency_Hz,Error_ppm,Temperature_C\n')
                
                print("24-Hour Stability Test Started")
                print("Logging every 60 seconds")
                print("Press Ctrl+C to stop\n")
                
                try:
                    while True:
                        line = ser.readline().decode('utf-8').strip()
                
                        if 'Frequency:' in line and 'Error:' in line:
                            # Parse frequency and error from Arduino output
                            # Example: "Frequency: 30000150 Hz Error: +5.000 ppm"
                
                            parts = line.split()
                            freq_hz = float(parts[1])
                            error_ppm = float(parts[4])
                
                            timestamp = datetime.now().isoformat()
                
                            log_file.write(f'{timestamp},{freq_hz},{error_ppm},25.0\n')
                            log_file.flush()
                
                            print(f'{timestamp}: {freq_hz} Hz ({error_ppm:+.3f} ppm)')
                
                        time.sleep(60)  # Log every minute
                
                except KeyboardInterrupt:
                    print("\n\nTest stopped by user")
                    log_file.close()
                    ser.close()
                
                    print("Generating report...")
                
                    # Generate summary
                    import pandas as pd
                    import matplotlib.pyplot as plt
                
                    df = pd.read_csv('24hr_stability.csv')
                
                    print("\n24-Hour Stability Report")
                    print("=" * 50)
                    print(f"Measurements: {len(df)}")
                    print(f"Mean frequency: {df['Frequency_Hz'].mean():.1f} Hz")
                    print(f"Std deviation: {df['Frequency_Hz'].std():.1f} Hz")
                    print(f"Min frequency: {df['Frequency_Hz'].min():.1f} Hz")
                    print(f"Max frequency: {df['Frequency_Hz'].max():.1f} Hz")
                    print(f"Peak-peak drift: {df['Frequency_Hz'].max() - df['Frequency_Hz'].min():.1f} Hz")
                    print(f"Mean error: {df['Error_ppm'].mean():.3f} ppm")
                    print(f"Error std dev: {df['Error_ppm'].std():.3f} ppm")
                    print("=" * 50)
                
                    # Plot
                    df['Timestamp'] = pd.to_datetime(df['Timestamp'])
                
                    plt.figure(figsize=(12, 8))
                
                    plt.subplot(2, 1, 1)
                    plt.plot(df['Timestamp'], df['Frequency_Hz'])
                    plt.ylabel('Frequency (Hz)')
                    plt.title('TinySA 30MHz Stability - 24 Hours')
                    plt.grid(True)
                
                    plt.subplot(2, 1, 2)
                    plt.plot(df['Timestamp'], df['Error_ppm'])
                    plt.ylabel('Error (ppm)')
                    plt.xlabel('Time')
                    plt.grid(True)
                
                    plt.tight_layout()
                    plt.savefig('24hr_stability.png', dpi=150)
                    print("\nPlot saved as: 24hr_stability.png")
                

Interpreting Results

Good Result:

Peak-peak drift: < 30 Hz (< 1 ppm)
                Standard deviation: < 10 Hz (< 0.3 ppm)
                Trend: Flat or slow linear drift
                Temperature sensitivity: < 0.5 ppm/°C
                

Typical Result:

Peak-peak drift: 30-150 Hz (1-5 ppm)
                Standard deviation: 10-50 Hz (0.3-1.7 ppm)
                Trend: Follows temperature
                Temperature sensitivity: 0.5-2 ppm/°C
                

Poor Result:

Peak-peak drift: > 300 Hz (> 10 ppm)
                Standard deviation: > 100 Hz (> 3 ppm)
                Trend: Erratic or large jumps
                → Possible crystal problem or loose connection
                

Temperature Characterization

Temperature Coefficient Measurement

Purpose: Quantify frequency vs. temperature relationship

Equipment: - Temperature sensor (DS18B20 or thermistor) - GPS frequency counter - Refrigerator and hair dryer

Procedure:

  1. Cold soak:
  2. Place TinySA in refrigerator
  3. Wait 30 minutes
  4. Measure frequency at 5°C

  5. Warm-up:

  6. Remove from fridge
  7. Measure frequency every 2 minutes
  8. Record temperature simultaneously
  9. Until reaches room temp (20°C)

  10. Heat test:

  11. Use hair dryer (gentle, low heat)
  12. Measure frequency every 2 minutes
  13. Record temperature
  14. Up to 40°C (don't overheat!)

  15. Cool-down:

  16. Turn off heat
  17. Measure as it cools
  18. Back to room temp

Data Analysis

import pandas as pd
                import numpy as np
                import matplotlib.pyplot as plt
                from scipy import stats
                
                # Load data
                df = pd.read_csv('temperature_test.csv')
                
                temp = df['Temperature_C'].values
                error_ppm = df['Error_ppm'].values
                
                # Linear fit
                slope, intercept, r_value, p_value, std_err = stats.linregress(temp, error_ppm)
                
                print("Temperature Coefficient Analysis")
                print("=" * 50)
                print(f"Temperature range: {temp.min():.1f} to {temp.max():.1f} °C")
                print(f"Frequency range: {error_ppm.min():.2f} to {error_ppm.max():.2f} ppm")
                print(f"Slope: {slope:.3f} ppm/°C")
                print(f"R²: {r_value**2:.4f}")
                print("=" * 50)
                
                # Plot
                plt.figure(figsize=(10, 6))
                plt.scatter(temp, error_ppm, alpha=0.5, label='Measurements')
                plt.plot(temp, slope*temp + intercept, 'r-', label=f'Fit: {slope:.3f} ppm/°C')
                plt.xlabel('Temperature (°C)')
                plt.ylabel('Frequency Error (ppm)')
                plt.title('Crystal Temperature Coefficient')
                plt.legend()
                plt.grid(True)
                plt.savefig('temperature_coefficient.png', dpi=150)
                plt.show()
                

Typical Coefficients

Crystal Type Temp Coefficient Frequency Change (0-40°C)
AT-cut (room temp) -0.035 ppm/°C/°C ±1-2 ppm
TCXO (cheap) -0.5 to -2 ppm/°C ±10-40 ppm
TCXO (good) -0.1 to -0.5 ppm/°C ±2-10 ppm
OCXO -0.001 to -0.01 ppm/°C ±0.02-0.2 ppm

Temperature Compensation

Once you know TC, compensate manually:

Current temp: 35°C
                Reference temp: 25°C
                Temperature coefficient: -0.8 ppm/°C
                ΔT = 35 - 25 = +10°C
                
                Frequency shift = -0.8 ppm/°C × 10°C = -8 ppm
                Apply correction: +8 ppm to compensate
                

Measurement Uncertainty Analysis

Sources of Uncertainty

GPS Method:

Source Uncertainty (ppm) Notes
GPS 1PPS jitter ±0.001 Negligible
Counter resolution ±0.00003 1 count in 30M
Temperature drift ±0.1-1.0 During measurement
Short-term noise ±0.01 Average multiple

Total (RSS): ±0.1-1.0 ppm (dominated by temperature)

WWV/CHU Method:

Source Uncertainty (ppm) Notes
Transmitter ±0.00001 Atomic clock
Ionosphere ±0.01-0.1 Doppler shift
Multipath ±0.05-0.5 Fading
Receiver ±0.1-1.0 Measurement

Total (RSS): ±0.1-1.0 ppm

FM Broadcast:

Source Uncertainty (ppm) Notes
Station accuracy ±0.01-2.0 GPS-locked vs. free
TinySA resolution ±0.1-1.0 At 100 MHz
Averaging ±0.5 Multiple stations

Total (RSS): ±1-5 ppm (depends on station quality)

Reducing Uncertainty

  1. Average multiple measurements: ``` σ_average = σ / sqrt(N)

Example: Single measurement: ±1 ppm Average 10 measurements: ±0.32 ppm Average 100 measurements: ±0.1 ppm ```

  1. Temperature stabilization:
  2. Let equipment warm up 30+ minutes
  3. Measure in stable environment
  4. Correct for temperature coefficient

  5. Multiple methods:

  6. GPS (primary)
  7. WWV/CHU (verify)
  8. FM (quick check)
  9. All should agree within combined uncertainty

Acceptance Testing

Final Validation Checklist

Before considering calibration complete:

Immediate Checks: - [ ] Error reduced from >10 ppm to <1 ppm - [ ] Calibration survives power cycle - [ ] Verified against second independent source - [ ] No unexpected jumps or instabilities

Short-Term Checks (1 hour): - [ ] Frequency stable to ±0.5 ppm over 1 hour - [ ] No large temperature swings - [ ] Measurements repeatable

Long-Term Checks (24 hours): - [ ] Drift < 1 ppm over 24 hours - [ ] Temperature correlation understood - [ ] No sudden frequency jumps

Documentation: - [ ] Recorded calibration date - [ ] Noted calibration method - [ ] Documented temperature coefficient - [ ] Saved reference measurements - [ ] Planned re-calibration date (1 year)


Periodic Re-Calibration

When to Re-Calibrate

Mandatory: - After firmware update (may reset cal) - After opening case (may affect crystal) - After drop or impact - If measurements show >2 ppm error

Recommended: - Every 6-12 months (crystal aging) - After extreme temperature exposure - Before critical measurements

Optional: - Before each use (paranoid) - Monthly (if doing precision work)

Quick Cal Check

Don't need full re-calibration - just verify:

1. Measure GPS 1PPS or CHU
                2. Note error
                3. If < 1 ppm: OK, no action
                4. If 1-3 ppm: Minor adjustment
                5. If > 3 ppm: Full re-calibration needed
                

Documentation Template

Calibration Record

TinySA Calibration Record
                =========================
                
                Date: _____________
                Operator: _____________
                Method: [ ] GPS  [ ] WWV/CHU  [ ] FM  [ ] Other:_______
                
                Initial Error: _______ Hz (_______ ppm) @ 30MHz
                Final Error: _______ Hz (_______ ppm) @ 30MHz
                
                Reference Source(s):
                1. __________________________
                2. __________________________
                3. __________________________
                
                Measurements:
                Before: _______ Hz
                After:  _______ Hz
                
                Temperature During Cal: _______°C
                Temperature Coefficient: _______ ppm/°C
                
                Verification:
                Method 1: _______ ppm error
                Method 2: _______ ppm error
                Agreement: ✓ / ✗
                
                24-Hour Stability: _______ ppm p-p
                
                Notes:
                _____________________________________________
                _____________________________________________
                
                Next Calibration Due: _____________
                
                Signature: _____________ Date: _____________
                

Troubleshooting Verification Failures

Problem: Large Discrepancy Between Methods

Example:

GPS shows: +5.2 ppm
                CHU shows: +11.8 ppm
                Difference: 6.6 ppm (too large!)
                

Possible causes: 1. GPS not locked (check 1PPS is blinking) 2. CHU propagation poor (multipath, fading) 3. Measurement error (recheck connections) 4. Temperature changed between measurements

Solution: - Re-measure with stable conditions - Use third method (FM) to arbitrate - Trust GPS if all other checks pass

Problem: Frequency Jumps

Example:

Measurement 1: 30,000,150 Hz
                Measurement 2: 30,000,148 Hz
                Measurement 3: 30,002,580 Hz ← Jump!
                Measurement 4: 30,000,149 Hz
                

Possible causes: 1. Loose connection (intermittent) 2. Power supply noise 3. Counter overflow (if using 16-bit counter) 4. Software bug

Solution: - Check all connections - Add decoupling capacitors - Use 32-bit counter - Re-upload firmware

Problem: Temperature Sensitivity Too High

Example:

Measured TC: -5.2 ppm/°C
                Expected: -0.5 to -2 ppm/°C
                

Possible causes: 1. Poor quality crystal 2. Aging crystal 3. Measurement error 4. Other temperature-sensitive component

Solution: - Re-measure carefully - Consider replacing crystal (if accessible) - Use tighter temperature control - Apply software compensation


Advanced Verification: Beat Frequency Method

Concept

Mix TinySA 30MHz with GPS-locked 30MHz reference, listen to beat note.

Setup:

GPS-locked 30MHz ──┐
                                   ├──→ Mixer ──→ Low-pass filter ──→ Audio amp ──→ Speaker
                TinySA 30MHz ──────┘
                
                Beat frequency = |f1 - f2|
                

If calibration perfect: - Beat frequency = 0 Hz (silence)

If error exists: - Beat frequency = error in Hz - Example: 150 Hz error → hear 150 Hz tone

Advantages: - Very sensitive (can hear 1 Hz difference) - Real-time monitoring - No counter needed

Disadvantages: - Requires building mixer circuit - Need GPS-locked 30MHz reference - Can't measure sign of error (only magnitude)


Summary

Verification Completed

✓ Immediate post-calibration check ✓ Cross-reference against multiple sources ✓ Short-term stability (1 hour) ✓ Long-term stability (24 hours) ✓ Temperature characterization ✓ Uncertainty analysis ✓ Acceptance testing ✓ Documentation

Key Metrics

Good Calibration: - Final error: < 1 ppm - Agreement between methods: < 0.5 ppm - 1-hour stability: < 0.5 ppm - 24-hour drift: < 1 ppm - Temperature coefficient: Known and compensated

Next Steps: - Use calibrated TinySA with confidence - Re-calibrate annually - Document all measurements


Your TinySA is now calibrated to atomic clock accuracy!