Digital Modulation
Est. read time: 3 minutes | Last updated: February 16, 2026 by John Gentile
Contents
- Phase Shift-Keying (PSK)
- Direct Sequence Spread Spectrum (DSSS)
- Orthogonal Frequency-Division Multiplexing (OFDM)
- References
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)
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 bits/sec, an bit spreading sequence/code produces an output chip rate . The length of the spreading code is also known as the Spreading Factor (SF), also equal to .
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:
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()

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))


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()

# 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))

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()

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))}")

Max correlation of 13.342580903015678 at 31

# 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()

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()

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

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))

1202
Orthogonal Frequency-Division Multiplexing (OFDM)
Orthogonal Frequency-Division Multiplexing
from IPython.display import YouTubeVideo
YouTubeVideo('1rpoUqx0360')
YouTubeVideo('UCRildDdrX4')