Est. read time: 3 minutes | Last updated: February 16, 2026 by John Gentile


Contents

Open In Colab

import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
from rfproto import filter, impairments, modulation, plot, sig_gen

Phase Shift-Keying (PSK)

Phase-shift keying- Wikipedia

Binary Phase-Shift Keying (BPSK)

Quadrature Phase-Shift Keying (QPSK)

Direct Sequence Spread Spectrum (DSSS)

DSSS spreads baseband signal energy over a wider spectral bandwidth using a spreading sequence. Receivers operate by cross-correlating the known, shared spreading sequence to despread the received DSSS signal.

Given an input bit rate or RbR_{b} bits/sec, an NN bit spreading sequence/code produces an output chip rate Rc=N×RbR_{c} = N \times R_{b}. The length of the spreading code is also known as the Spreading Factor (SF), also equal to RcRb\frac{R_{c}}{R_{b}}.

The signal spreading process also spreads any interference and noise across the entire bandwidth, effectively reducing its power relative to the main signal. We can calculate the processing gain of a spread spectrum signal with:

PdB=10log10(N)P_{dB} = 10\log_{10}(N)

Spreading provides benefits such as:

  • Code-Division Multiple Access (CDMA): where multiple users can reuse the same frequency band at the same time by using different spreading codes.
  • Jamming/interference resistance due to processing gain as well as spread bandwidth.
    • For example a jammer would have to jam the entire spread bandwidth or narrowband interference could be removed via notch filtering without much loss of information (especially if coding/forward-error-correction is used on the data).
    • Similar resistance to nominal channel fading exists since only a small portion of the signal will undergo fading at a given time.
  • Below-noise-floor signal reception for either low signal detection or low power usage like in GPS
  • Timing or ranging information between transmitter and receiver due to coherent correlation processes.
  • Resistant to multipath interference due to delayed versions of the spread signal having poor correlation with the main spread signal(as long as the multipath channel induces at least one chip of delay).
  • Depending on acquisition design of a receiver, can handle extremely high Doppler shifts.
barker_bits = np.array([1, 0, 1, 1, 0, 1, 1, 1, 0, 0, 0]) # 11-bit barker code
# NRZL encoding [1/0 -> +1/-1]
barker_nrz= 2 * barker_bits - 1

plot.bits(barker_nrz, "11-bit Barker Code")
plt.show()

png

data_bits = [1, 0, 1]
plot.bits(data_bits)
plt.show()

# Interpolate/upsample each bit by length of spread sequence
data_spread = np.repeat(data_bits, len(barker_bits))

plot.bits(data_spread)
plt.show()
print(len(data_spread))

png

png

33

for i in range(len(data_spread)):
    data_spread[i] ^= barker_bits[i % len(barker_bits)]

data_nrz = 2 * np.array(data_spread) - 1
plot.bits(data_nrz)
plt.show()

png

# sliding correlation of the transmitted sequence with the reference code:
# NOTE: inverted Barker NRZL spread code since direct XOR operation above
result = np.correlate(data_nrz, -barker_nrz, mode='full')
plt.plot(result)
plt.show()
print(max(result))
print(np.argmax(result))

png

11 10

tx = np.zeros(100)
tx[21:21+len(data_nrz)] = data_nrz 
tx += np.random.normal(-0.5, 0.5, 100)
plt.plot(tx)
plt.show()

png

result = np.correlate(tx, -barker_nrz, mode='full')
plt.plot(result)
plt.show()
print(f"Max correlation of {max(result)} at {list(result).index(max(result))}")

png

Max correlation of 13.342580903015678 at 31

image.png

# CCSDS 64b ASM (https://ccsds.org/Pubs/131x0b5.pdf)
ccsds_asm_64b = 0x034776C7272895B0
# Convert from 64b number to bit string
asm_bits = [(ccsds_asm_64b >> i) & 1 for i in range(63, -1, -1)]
plot.bits(asm_bits)
plt.show()

png

asm_qpsk_symbols = []
for i in range(len(asm_bits) // 2):
    sym = (asm_bits[i*2] << 1) | asm_bits[(i*2)+1]
    asm_qpsk_symbols.append(sym)

print(asm_qpsk_symbols)

[0, 0, 0, 3, 1, 0, 1, 3, 1, 3, 1, 2, 3, 0, 1, 3, 0, 2, 1, 3, 0, 2, 2, 0, 2, 1, 1, 1, 2, 3, 0, 0]

# Generate 100 random QPSK payload symbols
rand_symbols = np.random.randint(0, 4, 100)
packet_symbols = []
packet_symbols.extend(asm_qpsk_symbols)
packet_symbols.extend(rand_symbols)
print(packet_symbols)

[0, 0, 0, 3, 1, 0, 1, 3, 1, 3, 1, 2, 3, 0, 1, 3, 0, 2, 1, 3, 0, 2, 2, 0, 2, 1, 1, 1, 2, 3, 0, 0, np.int64(2), np.int64(0), np.int64(0), np.int64(2), np.int64(1), np.int64(0), np.int64(3), np.int64(3), np.int64(2), np.int64(1), np.int64(2), np.int64(1), np.int64(0), np.int64(0), np.int64(1), np.int64(3), np.int64(3), np.int64(1), np.int64(0), np.int64(3), np.int64(1), np.int64(1), np.int64(2), np.int64(1), np.int64(2), np.int64(0), np.int64(1), np.int64(3), np.int64(2), np.int64(1), np.int64(3), np.int64(2), np.int64(2), np.int64(0), np.int64(1), np.int64(2), np.int64(1), np.int64(0), np.int64(1), np.int64(2), np.int64(3), np.int64(1), np.int64(0), np.int64(0), np.int64(0), np.int64(0), np.int64(1), np.int64(1), np.int64(3), np.int64(0), np.int64(3), np.int64(3), np.int64(2), np.int64(1), np.int64(1), np.int64(2), np.int64(3), np.int64(2), np.int64(0), np.int64(0), np.int64(1), np.int64(1), np.int64(2), np.int64(1), np.int64(1), np.int64(0), np.int64(2), np.int64(1), np.int64(3), np.int64(3), np.int64(0), np.int64(0), np.int64(2), np.int64(2), np.int64(1), np.int64(1), np.int64(3), np.int64(0), np.int64(1), np.int64(2), np.int64(2), np.int64(0), np.int64(0), np.int64(0), np.int64(1), np.int64(0), np.int64(2), np.int64(2), np.int64(1), np.int64(2), np.int64(1), np.int64(0), np.int64(3), np.int64(2), np.int64(3), np.int64(1), np.int64(3), np.int64(0), np.int64(3), np.int64(1)]

sym_rate = 1e6             # Baseband symbol rate
L        = 4               # Upsample ratio (Samples per Symbol)
fs       = L * sym_rate    # Output sample rate (Hz)

rolloff          = 0.5     # Alpha of RRC
num_filt_symbols = 6       # Symbol length of RRC matched filter

qpsk_tx_filtered = sig_gen.gen_mod_signal(
    "QPSK",
    packet_symbols,
    fs,
    sym_rate,
    "RRC",
    rolloff,
    num_filt_symbols,
)
test_sig = impairments.awgn(-30, 12 * len(qpsk_tx_filtered))
phase_offset = int(2.1 * len(qpsk_tx_filtered))
test_sig[phase_offset:phase_offset+len(qpsk_tx_filtered)] += qpsk_tx_filtered
print(f"Phase offset: {phase_offset}")

Phase offset: 1100

plot.spec_an(test_sig, fs=fs, fft_shift=True, show_SFDR=False, y_unit="dB")
plt.show()

png

plt.specgram(test_sig, pad_to=1024, Fs=fs)
plt.xlabel('Time (s)')
plt.ylabel('Frequency (Hz)')
plt.show()

png

mod = modulation.MPSKModulation(4)
asm_iq = mod.modulate(asm_qpsk_symbols)
print(asm_iq)

[ 1.+1.j 1.+1.j 1.+1.j -1.-1.j -1.+1.j 1.+1.j -1.+1.j -1.-1.j -1.+1.j -1.-1.j -1.+1.j 1.-1.j -1.-1.j 1.+1.j -1.+1.j -1.-1.j 1.+1.j 1.-1.j -1.+1.j -1.-1.j 1.+1.j 1.-1.j 1.-1.j 1.+1.j 1.-1.j -1.+1.j -1.+1.j -1.+1.j 1.-1.j -1.-1.j 1.+1.j 1.+1.j]

asm_iq_upsampled = np.repeat(asm_iq, L)
print(asm_iq_upsampled)

[ 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j -1.+1.j 1.-1.j 1.-1.j 1.-1.j 1.-1.j -1.-1.j -1.-1.j -1.-1.j -1.-1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j 1.+1.j]

asm_fft = np.conj(np.fft.fft(asm_iq_upsampled))

corrs = []
for i in range(len(test_sig) // len(asm_fft)):
    start_idx = i * len(asm_fft)
    end_idx = (i + 1) * len(asm_fft)
    xcorr = np.fft.ifft(np.fft.fft(test_sig[start_idx:end_idx]) * asm_fft)
    corrs.extend(np.abs(xcorr) ** 2)

plt.plot(corrs)
plt.show()
print(np.argmax(corrs))

png

1202

Orthogonal Frequency-Division Multiplexing (OFDM)

Orthogonal Frequency-Division Multiplexing

from IPython.display import YouTubeVideo
YouTubeVideo('1rpoUqx0360')
YouTubeVideo('UCRildDdrX4')

References