Classroom Glossary Public page

WIR-101 Lab 8 — Sub-GHz Spectrum Survey and IQ Analysis

567 words

Prerequisites: Week 8 lecture; RTL-SDR v4 installed (rtl_test passes); GNU Radio + Python NumPy/SciPy/Matplotlib Duration: ~90 min Points: 100


Authorization

  • Lab Authorization Form signed
  • Receive only; no transmission in this lab
  • Survey frequencies: 300-450 MHz sub-GHz ISM band only
  • No capture, decode, or replay of communications not expressly authorized

Objective

Use the RTL-SDR to perform a sub-GHz spectrum survey, identify at least two signal types from their modulation characteristics, record a 30-second IQ capture, and perform basic signal analysis in Python: FFT spectrum plot, amplitude envelope, and IQ constellation.


Part A — Sub-GHz Survey with rtl_power (20 min)

Run a wide-band power sweep across the sub-GHz ISM range:

rtl_power -f 300000000:450000000:25000 -g 40 -1 survey_300_450.csv

Then visualize:

# survey_plot.py
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np

df = pd.read_csv('survey_300_450.csv', header=None)

freqs = []
powers = []
for _, row in df.iterrows():
    start = float(row[2])
    step = float(row[4])
    vals = [float(x) for x in row[6:] if str(x).strip()]
    freqs.extend([start + i*step for i in range(len(vals))])
    powers.extend(vals)

freq_mhz = [f/1e6 for f in freqs]

plt.figure(figsize=(16,5))
plt.plot(freq_mhz, powers, linewidth=0.4, color='steelblue')
plt.xlabel('Frequency (MHz)')
plt.ylabel('Power (dB)')
plt.title('Sub-GHz Survey 300-450 MHz')
plt.grid(True, alpha=0.3)
plt.axvline(315, color='r', linestyle='--', alpha=0.5, label='315 MHz (US remotes)')
plt.axvline(433.92, color='g', linestyle='--', alpha=0.5, label='433.92 MHz (EU sensors)')
plt.legend()
plt.tight_layout()
plt.savefig('survey_300_450.png', dpi=150)
print("Saved survey_300_450.png")

Annotate the plot (in Python or in an image editor) marking any peaks that appear significantly above the noise floor.

Deliverable A

survey_300_450.png (annotated) + a table listing every peak you identified above the noise floor (frequency in MHz, approximate peak power in dB, likely source if identifiable).


Part B — Targeted IQ Capture (20 min)

Pick the strongest peak you observed in Part A (or use 433.92 MHz if you see activity there; use 315 MHz if you are in the US with sub-GHz remote activity nearby).

Capture 30 seconds of IQ data at that frequency:

# Using rtl_sdr (raw IQ output)
rtl_sdr -f 433920000 -s 250000 -g 40 -n $((250000 * 30)) target_433.iq

Or use GQRX: Device → Record → I/Q recording to .iq file.

The resulting file contains 32-bit float pairs (I, Q) interleaved. File size should be approximately:

  • 250 kSps × 30 s × 8 bytes/sample (2× float32) = 60 MB

Confirm:

ls -lh target_433.iq  # should be ~60 MB

Virtual Path: The course portal provides target_433_virtual.iq -- a pre-captured 30-second recording from a 433 MHz environment. Download and use it in place of the live capture.

Deliverable B

Confirm file size + SHA256 checksum of your capture (or the virtual path file):

sha256sum target_433.iq

Part C — FFT Spectrum Analysis in Python (20 min)

# fft_analysis.py
import numpy as np
import matplotlib.pyplot as plt

sample_rate = 250000  # samples per second
center_freq = 433.92e6  # Hz

# Load IQ data
samples = np.fromfile('target_433.iq', dtype=np.complex64)
print(f"Loaded {len(samples):,} samples ({len(samples)/sample_rate:.1f} seconds)")

# FFT of first 8192 samples (one window)
fft_size = 8192
window = samples[:fft_size] * np.blackman(fft_size)
spectrum = np.fft.fftshift(np.fft.fft(window, fft_size))
freqs = np.fft.fftshift(np.fft.fftfreq(fft_size, d=1/sample_rate)) + center_freq

power_db = 20 * np.log10(np.abs(spectrum) + 1e-10)

plt.figure(figsize=(12,4))
plt.plot(freqs/1e6, power_db, linewidth=0.5)
plt.xlabel('Frequency (MHz)')
plt.ylabel('Power (dBFS)')
plt.title(f'FFT Spectrum @ {center_freq/1e6} MHz (8192-pt Blackman window)')
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('fft_spectrum.png', dpi=150)

Answer:

  1. Where is the signal peak relative to the center frequency?
  2. What is the approximate signal bandwidth (measure the -10 dB bandwidth)?

Deliverable C

fft_spectrum.png + answers to the two questions.


Part D — Amplitude Envelope and Modulation Identification (15 min)

# amplitude_analysis.py
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import butter, filtfilt

sample_rate = 250000
samples = np.fromfile('target_433.iq', dtype=np.complex64)

# Take a 1-second slice where the signal is active
# Adjust start_sample to find a burst; use GQRX/Inspectrum to identify the approximate time
start_sample = 50000  # adjust based on your capture
slice_samples = samples[start_sample:start_sample + sample_rate]

# Amplitude envelope
amplitude = np.abs(slice_samples)

# Smooth with LPF
b, a = butter(4, 5000/(sample_rate/2), btype='low')
smoothed = filtfilt(b, a, amplitude)

# IQ constellation
plt.figure(figsize=(14,4))

plt.subplot(1,3,1)
plt.plot(amplitude[:10000])
plt.title('Amplitude Envelope (raw)')
plt.xlabel('Sample')

plt.subplot(1,3,2)
plt.plot(smoothed[:10000])
plt.title('Amplitude Envelope (LPF)')
plt.xlabel('Sample')

plt.subplot(1,3,3)
plt.scatter(slice_samples.real[:5000], slice_samples.imag[:5000],
            alpha=0.1, s=1, c='steelblue')
plt.scatter([0], [0], c='red', s=20, zorder=5)
plt.xlabel('I')
plt.ylabel('Q')
plt.title('IQ Constellation')
plt.axis('equal')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('amplitude_iq.png', dpi=150)

From the plots, identify the modulation type:

  • ASK/OOK: amplitude clearly switches between on/off (two amplitude levels); IQ constellation shows two clusters (one near origin, one offset)
  • FSK: amplitude is relatively constant; two frequency blobs visible in spectrum; IQ constellation is a ring (constant amplitude, varying phase)
  • BPSK: amplitude constant; two phase clusters at 0° and 180° in IQ constellation

Deliverable D

amplitude_iq.png + 1 paragraph: what modulation type is this signal? What evidence from the plots supports your conclusion?


Write-up Questions

  1. Your RTL-SDR has a maximum sample rate of 2.4 Msps. The 802.11n channel bandwidth at 2.4 GHz is 20 MHz. Can you capture a full 802.11n channel with this SDR? What sample rate would you need, and what SDR hardware supports it?
  2. The Blackman window was applied before the FFT in Part C. What would the spectrum look like without any window? Why is windowing used?
  3. You identified an ASK signal on 433 MHz. The signal's bit rate appears to be approximately 2400 bits/sec from the amplitude envelope transitions. What is the minimum channel bandwidth required for this signal (using Nyquist's minimum bandwidth theorem: BW = bit_rate / 2)?
  4. Inspectrum and GQRX both show the same IQ capture as a time-frequency waterfall. What information does the waterfall show that a single FFT frame does not?

Submission

Zip into lab8_YOURNAME.zip:

  • survey_300_450.png (annotated)
  • deliverable_A_table.md
  • deliverable_B_checksum.txt
  • fft_spectrum.png
  • deliverable_C_answers.md
  • amplitude_iq.png
  • deliverable_D.md
  • survey_plot.py, fft_analysis.py, amplitude_analysis.py
  • writeup.md