Digital Modulation
Est. read time: 4 minutes | Last updated: March 09, 2026 by John Gentile
Contents
- Chirp
- 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, measurements, modulation, plot, sig_gen
Chirp
A chirp is a signal where the frequency increases (up-chirp) or decreases (down-chirp) with time, (also known as a frequency sweep).
Linear Frequency Modulated (LFM) Chirp
In LFM chirp, the instantaneous frequency, (in Hz), varies linearly with time:
where is the starting frequency (Hz), and is the constant chirp rate given an end frequency (Hz) and the sweep time between frequencies :
Since frequency is the derivative of phase (e.g. ), and frequency is linearly changing (increasing or decreasing), it is expected that phase changes quadratic over time, as shown by:
The corresponding time-domain output is simply the of this phase function, or for complex output.
f_start = 10e3
f_end = 40e3
fs = 100e3
num_samples = 10000
lfm_chirp_sig = sig_gen.cmplx_dt_lfm_chirp(1, f_start, f_end, fs, num_samples)
freq, y_PSD = measurements.PSD(lfm_chirp_sig, fs, norm=True)
plot.freq_sig(freq, y_PSD, "LFM Chirp Spectrum", scale_noise=True)
plt.show()

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 11.803423315663316 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(1), np.int64(2), np.int64(2), np.int64(0), np.int64(2), np.int64(0), np.int64(3), np.int64(1), np.int64(1), np.int64(1), np.int64(1), np.int64(1), np.int64(0), np.int64(3), np.int64(3), np.int64(3), np.int64(3), np.int64(3), np.int64(2), np.int64(0), np.int64(2), np.int64(1), np.int64(3), np.int64(3), np.int64(2), np.int64(2), np.int64(3), np.int64(3), np.int64(1), np.int64(3), np.int64(1), np.int64(3), np.int64(0), np.int64(2), np.int64(1), np.int64(1), np.int64(1), np.int64(1), np.int64(0), np.int64(1), np.int64(1), np.int64(0), np.int64(3), np.int64(3), np.int64(1), np.int64(1), np.int64(2), np.int64(0), np.int64(1), np.int64(0), np.int64(1), np.int64(3), np.int64(2), np.int64(0), np.int64(0), np.int64(2), np.int64(2), np.int64(1), np.int64(2), np.int64(3), np.int64(2), np.int64(0), np.int64(1), np.int64(3), np.int64(3), np.int64(1), np.int64(2), np.int64(1), np.int64(1), np.int64(3), np.int64(3), np.int64(3), np.int64(0), np.int64(2), np.int64(1), np.int64(3), np.int64(3), np.int64(0), np.int64(3), np.int64(3), np.int64(1), np.int64(0), np.int64(2), np.int64(3), np.int64(0), np.int64(1), np.int64(0), np.int64(3), np.int64(1), np.int64(0), np.int64(1), np.int64(3), np.int64(1), np.int64(3), np.int64(2), np.int64(2), np.int64(0), np.int64(1), np.int64(3), np.int64(2)]
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))

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