"The enemy will learn your frequency. The question is how long it takes him -- and how much it costs him when he does." -- paraphrase of FCC Part 15 spread spectrum rationale; the civilian version of a military problem that has been solved and re-solved for seventy years
Lecture (90 min)
8.1 Why Waveform Agility
Weeks 1-7 built a SIGINT pipeline: receive a signal, classify its modulation, decode its bit stream, hypothesize its protocol. That pipeline works against fixed-frequency, known-modulation targets. But a transmitter designed to evade SIGINT does not cooperate.
Three real scenarios:
GPS jamming at sea. A commercial GPS receiver operates at 1575.42 MHz (L1) with a spreading gain of 43 dB. Jamming GPS L1 with a $50 wideband noise generator at 10 watts overwhelms civilian receivers within several kilometers. Military GPS (L1P(Y)) uses an encrypted PN sequence with a much higher chip rate -- the spreading gain is higher and the PN code is unknown to the jammer, so the jammer cannot correlate against it and must transmit much more power to interfere.
FHSS TPMS evasion. Tire pressure monitoring systems on vehicles transmit at 433 MHz using OOK or FSK. Some vendors have moved to FHSS to avoid accidental correlation with neighboring vehicles' sensors. The frequency-hopping pattern is synchronized between the wheel transmitter and the car's receiver; an uninvited receiver must scan the full hop set to reassemble the signal.
Chirp radar detection. A linear frequency-modulated (LFM) radar pulse sweeps from 9.3 GHz to 9.5 GHz in 1 microsecond. Any wideband receiver sees it as a brief noise burst. The matched filter at the radar receiver compresses the chirp into a narrow pulse, recovering the range resolution of a pulse much shorter than 1 microsecond. A SIGINT receiver without the matched filter cannot resolve the target.
These three scenarios share a structure: the transmitter trades simplicity for a property the receiver needs (resistance to jamming, resistance to interception, range resolution), and an adversary receiver needs additional signal processing to recover the information.
8.2 Frequency-Hopping Spread Spectrum (FHSS)
In FHSS, the transmitter changes its carrier frequency at each hop according to a pseudo-random sequence known to both transmitter and receiver. A narrowband jammer targeting one frequency jams only the fraction of hops that land on that frequency; a wideband jammer must spread its power across the full hop set.
Key parameters:
| Parameter | Symbol | Typical values |
|---|---|---|
| Hop rate | R_h |
100 -- 1600 hops/sec |
| Number of hop channels | N_h |
16 -- 256 |
| Channel spacing | Δf |
100 kHz -- 1 MHz |
| Total hopping bandwidth | B_ss = N_h × Δf |
1.6 MHz -- 256 MHz |
| Coherence time per hop | T_h = 1/R_h |
0.6 -- 10 ms |
Synchronization requirement: The receiver must know the hop sequence (PN code) and the current phase within that sequence (timing synchronization within one hop period). This is the fundamental security assumption: the PN code is secret, and the receiver that knows the code can follow the hops.
SIGINT implication: A Stage 3 multi-access identification (from Week 7) would detect the hopping pattern in the time-frequency map -- the signal looks like a fast-moving frequency. But recovering the hop sequence itself requires either knowing the PN code or observing enough hops to statistically reconstruct the sequence. Lab 8 Part A demonstrates hop sequence recovery from the spectrogram.
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import spectrogram
def simulate_fhss(n_channels=16, hop_rate=1600, fs=4e6, duration=0.1):
"""
Simulate a 16-channel FHSS signal.
Returns IQ samples at sample rate fs for `duration` seconds.
"""
# Hop channel frequencies: evenly spaced across 1.6 MHz
f_min = -0.8e6
f_max = 0.8e6
hop_freqs = np.linspace(f_min, f_max, n_channels)
samples_per_hop = int(fs / hop_rate)
total_samples = int(fs * duration)
# Generate a deterministic but pseudo-random hop sequence
rng = np.random.default_rng(seed=42) # fixed seed for reproducibility
n_hops = int(duration * hop_rate)
hop_sequence = rng.integers(0, n_channels, size=n_hops)
iq = np.zeros(total_samples, dtype=np.complex64)
for i, hop_idx in enumerate(hop_sequence):
start = i * samples_per_hop
end = min(start + samples_per_hop, total_samples)
n = end - start
t = np.arange(n) / fs
f = hop_freqs[hop_idx]
# Add narrowband tone at the hop frequency
iq[start:end] = np.exp(1j * 2 * np.pi * f * t).astype(np.complex64)
return iq, hop_sequence, hop_freqs
def plot_fhss_spectrogram(iq, fs, hop_rate, title="FHSS Spectrogram"):
f, t, Sxx = spectrogram(iq, fs=fs, nperseg=256, noverlap=200, return_onesided=False)
f_shifted = np.fft.fftshift(f)
Sxx_shifted = np.fft.fftshift(Sxx, axes=0)
plt.figure(figsize=(12, 6))
plt.pcolormesh(t * 1000, f_shifted / 1e3, 10 * np.log10(Sxx_shifted + 1e-10),
shading='gouraud', cmap='hot')
plt.xlabel("Time (ms)")
plt.ylabel("Frequency (kHz)")
plt.colorbar(label="Power (dB)")
plt.title(title)
plt.tight_layout()
plt.savefig("fhss_spectrogram.png", dpi=150)
return f_shifted, t, Sxx_shifted
8.3 Chirp Spread Spectrum (CSS) and Processing Gain
A chirp signal sweeps linearly from frequency f_0 to f_0 + BW over a duration T. The time-bandwidth product TW = T × BW determines the processing gain:
PG_chirp = 10 × log10(TW) [dB]
For LoRa with BW = 125 kHz and T = 2.048 ms: TW = 2048 × 125 = 256, PG = 24 dB.
Matched filter receiver: A receiver correlates the incoming signal against a local replica of the chirp waveform. When the incoming signal matches the replica, the correlation output peaks. The peak's SNR is TW times larger than the SNR of the raw received chirp.
def generate_chirp(f_start, f_end, T, fs):
"""Generate a linear frequency-modulated (LFM) chirp signal."""
t = np.arange(int(T * fs)) / fs
k = (f_end - f_start) / T # chirp rate (Hz/sec)
phase = 2 * np.pi * (f_start * t + 0.5 * k * t**2)
return np.exp(1j * phase).astype(np.complex64)
def matched_filter_receive(received, chirp_template):
"""
Apply matched filter: correlate received signal with conjugated template.
Returns the correlation output; peak marks the start of the chirp.
"""
template_conj = np.conj(chirp_template[::-1])
return np.convolve(received, template_conj, mode='full')
# Processing gain verification
def verify_processing_gain(BW, T):
TW = T * BW
PG_dB = 10 * np.log10(TW)
print(f"BW={BW/1e3:.0f} kHz, T={T*1e3:.3f} ms")
print(f"TW = {TW:.1f}, PG = {PG_dB:.1f} dB")
return PG_dB
8.4 Direct-Sequence Spread Spectrum (DSSS)
In DSSS, each data bit is multiplied by a PN (pseudo-noise) sequence of N chips. The data rate is spread from R_data to N × R_data. A receiver that knows the PN code despreads the signal by correlating against the code; a receiver that does not know the code sees a wideband noise signal.
Spreading and despreading:
def dsss_spread(data_bits, pn_sequence):
"""Spread data_bits using pn_sequence (BPSK: 0 → -1, 1 → +1)."""
data_bpsk = 2 * data_bits - 1 # 0 → -1, 1 → +1
pn_bpsk = 2 * pn_sequence - 1
spread = np.repeat(data_bpsk, len(pn_sequence)) * np.tile(pn_bpsk, len(data_bits))
return spread
def dsss_despread(received_chips, pn_sequence):
"""Despread by correlating each chip block against the PN sequence."""
L = len(pn_sequence)
pn_bpsk = 2 * pn_sequence - 1
n_bits = len(received_chips) // L
recovered = np.zeros(n_bits)
for i in range(n_bits):
block = received_chips[i*L:(i+1)*L]
correlation = np.dot(block, pn_bpsk) / L
recovered[i] = 1 if correlation > 0 else 0
return recovered
Jamming margin: The advantage of DSSS against a narrowband jammer:
J/S (dB) = PG (dB) - (Eb/N0)_required (dB)
where PG = 10 × log10(N) is the spreading gain (chips per bit), and (Eb/N0)_required is the minimum SNR needed to decode the data. A DSSS system with PG = 20 dB and (Eb/N0)_required = 10 dB has a jamming margin of 10 dB: a jammer must transmit 10× more power than the desired signal to cause a bit error rate increase.
8.5 LPI and LPD Waveform Taxonomy
Low Probability of Interception (LPI) and Low Probability of Detection (LPD) are related but distinct objectives:
| Property | Goal | Primary technique |
|---|---|---|
| LPD | Adversary cannot detect that a transmission occurred | Power minimization, burst mode, directional antennas |
| LPI | Adversary cannot determine the information content | Spread spectrum (FHSS, DSSS, chirp), encryption |
| Waveform | LPD | LPI | Processing gain | Synchronization requirement |
|---|---|---|---|---|
| FHSS | Moderate (burst energy spread) | High (unknown hop sequence) | 10·log10(N_h) |
Tight (per-hop timing) |
| DSSS | Low (wideband energy visible) | High (unknown PN code) | 10·log10(N_chips) |
Chip-level timing |
| Chirp (CSS) | Low (wideband pulse visible) | Moderate (matched filter needed) | 10·log10(TW) |
Chirp start timing |
| Hybrid FHSS+DSSS | High | Very high | Combined | Very demanding |
8.6 Architecture Comparison Sidebar
| Waveform | Deployment | Hop/chip rate | Processing gain | Security model |
|---|---|---|---|---|
| GPS L1 C/A | Civilian navigation | 1.023 Mchips/sec DSSS | ~43 dB | Public PN code; no secrecy |
| GPS L1 P(Y) | Military navigation | 10.23 Mchips/sec DSSS | ~53 dB | Classified PN; anti-spoof |
| IEEE 802.11 DSSS (legacy) | WiFi (1-2 Mbps) | 11 Mchips/sec | ~10.4 dB | Public code; WEP/WPA security above |
| Bluetooth BR | PAN | 1600 hops/sec, 79 channels | ~18.9 dB | AFH (adaptive, avoids occupied channels) |
| LoRa CSS | IoT | BW=125-500 kHz, SF=7-12 | 18-37 dB (depends on SF) | Chirp parameters are public |
| 5G NR PUSCH | Cellular uplink | Frequency hopping per slot | Varies (OFDM subcarrier) | 5G-AKA authentication above |
| Military FHSS | Tactical radio (HAVE QUICK, SATURN) | >1000 hops/sec | High | Classified hop set |
Key observation for SIGINT work: GPS L1 C/A and LoRa use public spreading codes -- the processing gain provides interference resistance but not information secrecy. Military FHSS uses classified hop sequences -- the processing gain provides both. The civilian IoT world sits somewhere between: many proprietary 433 MHz systems use simple FHSS with synchronized counters as the "PN code," which is recoverable by Lab 9's protocol RE techniques.
8.7 Anti-Jamming Margin Budget
A system-level anti-jamming budget for an FHSS system:
| Parameter | Value | Notes |
|---|---|---|
| Required SNR for BER 10^-3 | 10 dB | GFSK, Sklar Table B-3 |
| Processing gain (PG) | 10·log10(N_h) |
N_h = 16 → PG = 12 dB |
| Jammer-to-signal ratio (J/S) | PG - SNR_req | 12 - 10 = 2 dB |
| Interpretation | Jammer can transmit only 1.6× more power | Very modest margin |
Increasing to 256 channels (PG = 24 dB) gives J/S = 14 dB -- the jammer must transmit 25× more power. The anti-jamming margin scales logarithmically with the number of hop channels.
Sklar Ch 13 weave. Sklar's analysis in Ch 13 ("Spread-Spectrum Techniques") derives this budget formally from the link equation. The key result is that FHSS buys jamming margin proportional to the hop bandwidth relative to the data bandwidth -- the same ratio that appears as processing gain in DSSS and as TW in chirp systems. The three waveforms are mathematically unified by this ratio.
Lab Preview
Lab 8 applies these concepts: FHSS simulation in Python, matched-filter verification of chirp processing gain, and a jamming margin budget calculation. See labs/lab-8.md.
Toolchain Diary Prompt
New this week: scipy.signal.spectrogram for time-frequency visualization; numpy.random.default_rng for reproducible PN sequence generation; GNU Radio FHSS Synthesizer block. Compare the matplotlib spectrogram of your simulated FHSS signal to the gr-fosphor display of a real FHSS signal from Lab 7 (if your unknown signal was FHSS-class). Note: what would Stage 3 of the Lab 7 SIGINT pipeline say about an FHSS signal it had never seen before?