A fully simulated OFDM transceiver in GNU Radio: baseband generation, RF-like up/down-conversion, an AWGN channel, and demodulation — every stage visualized, and the link's noise tolerance measured end-to-end rather than assumed.
Skills demonstrated: GNU Radio flowgraph design · OFDM PHY design (802.11-style numerology) · IQ modulation/demodulation · AWGN channel modeling · FIR filter design · DSP signal analysis · Python / Jupyter data analysis
- Parameters match real IEEE 802.11a/g PHY numerology: 64-pt FFT, 16-sample (25%) cyclic prefix, pilots at ±7/±21
- Full TX → channel → RX chain visualized and cross-checked at 8 stages, time and frequency domain, including image-frequency/LO-leakage artifacts and why they force a decimating LPF
- Measured a complete packet-delivery-rate waterfall curve across 30 noise levels (0.5–6.5): 99.96% → 0%, 50%-point at noise ≈3.95
- Diagnosed GNU Radio's CRC32-gated all-or-nothing packet delivery from first principles, then confirmed it empirically — every delivered packet is byte-exact even at 2 of 9990 packets received
- Derived the link's raw PHY rate (~7.68 kbps, ~1.2 bits/s/Hz) directly from the FFT/CP/modulation parameters and combined it with measured PDR into actual goodput numbers, not just a delivery percentage
- Results validated with an automated TX/RX comparison in a companion notebook (
analysis/visualize_results.ipynb), not just visual inspection of scopes
- Project Goal
- System Architecture
- Simulation Parameters
- Design Decisions & Trade-offs
- Signal Visualization
- Results: Packet Delivery Rate vs. Channel Noise
- Getting Started
- Repository Structure
- Possible Improvements
Build a hands-on, end-to-end understanding of an OFDM transceiver — signal generation, RF up/down-conversion, channel impairments, demodulation — by implementing, visualizing, and measuring every stage in GNU Radio.
A complete OFDM link: transmitter (IFFT + cyclic prefix), a noisy channel, and a receiver that reverses the process via quadrature demodulation, LPF filtering, and FFT-based OFDM demodulation. Every block below is verified in isolation (Signal Visualization) and end-to-end (Results).
| Parameter | Value |
|---|---|
| FFT length | 64 |
| Subcarrier spacing | 100 Hz (= 6.4 kHz / 64) |
| Cyclic prefix length | 16 samples (25% of the useful symbol) |
| Useful / total OFDM symbol duration | 10 ms / 12.5 ms |
| Pilot subcarriers | -21, -7, 7, 21 |
| Data subcarriers | 48 (confirmed via oc_carriers) |
| Occupied subcarriers (total) | 52 of 64 (48 data + 4 pilot; DC + 11 nulled) |
| Header modulation | BPSK |
| Payload modulation | QPSK |
| Baseband sample rate | 6.4 kSa/s |
| Simulated "RF" sample rate | 140.8 kSa/s (×22 interpolation) |
| Simulated carrier frequency | 16 kHz |
| Channel model | AWGN, amplitude swept 0.5–6.5 (see Results) |
Design note: these aren't arbitrary numbers — 64-pt FFT, a 16-sample (25%) cyclic prefix, and pilots at exactly ±7/±21 is the same numerology IEEE 802.11a/g uses, just re-scaled to a low, easily-visualized sample rate.
Derived PHY throughput: with the confirmed 48 data-subcarrier layout (oc_carriers), QPSK payload gives 48 × 2 = 96 bits per OFDM symbol; at a 12.5 ms symbol period that's a raw payload PHY rate of 96 / 0.0125 s ≈ 7.68 kbps, or ≈1.2 bits/s/Hz across the 6.4 kHz channel — the standard way 802.11-style rate tables are computed (data subcarriers × bits/symbol / symbol duration), independent of packet framing overhead. Combined with the measured PDR curve below, this converts directly to goodput: ~7.68 kbps near-ideal (noise=0.5), ~3.84 kbps at the 50%-PDR point (noise≈3.95), and <0.2 kbps by noise=5.0.
- 25% CP overhead — matches the 802.11a/g guard-interval ratio; protects against delay spread up to 2.5 ms here at the cost of 25% throughput.
- BPSK header / QPSK payload — the header carries critical framing and stays on the most robust constellation; the payload trades robustness for 2× throughput. Standard practice: PHY headers are sent robust regardless of the negotiated payload rate.
- Pilots at ±7/±21 — the same 802.11a/g layout for tracking residual frequency/phase offset across the band.
- AWGN, not multipath — isolates and quantifies SNR degradation cleanly (see Results) before adding fading complexity (Possible Improvements).
- Reduced sample rate (6.4 / 140.8 kSa/s) — keeps every ratio that matters (interpolation factor, filter cutoff-to-Fs, carrier-to-Fs) representative of a real IQ front-end, fully visualizable without SDR hardware.
On the packaged OFDM Tx/Rx blocks: OFDM Transmitter/OFDM Receiver are GNU Radio hier_block2 wrappers — internally they run sync, channel estimation, equalization, decoding, and a CRC32 integrity check, but only expose baseband-in / bytes-out. Two consequences: (1) any payload failing its CRC is dropped whole, not passed through corrupted — see Results for the empirical proof; (2) the pre-decision equalized symbol stream (needed for a constellation plot) isn't exposed — getting it means rebuilding the RX chain from primitive Sync/Channel-Estimator/Equalizer/Serializer blocks instead.
Each stage below is grounded in the actual captured plot, following the signal from bitstream to recovered data.
1. Carrier waveform — 16 kHz local oscillator for later I/Q conversion; confirmed directly from the trace (~10 cycles / 0.6 ms).

2. Baseband OFDM (after modulation) — straight out of the OFDM Transmitter (64-pt IFFT + 16-sample CP). The noise-like envelope is OFDM's characteristic high PAPR; packet_len: 560 markers show GNU Radio's Tagged Stream framing in action.

3. Up-converted to carrier — after the Rational Resampler + I/Q mixing. Individual OFDM symbol envelopes are visible riding the 16 kHz carrier: the same PAPR, now visible at "RF".

4. Channel effect (A/B) — same tap point, AWGN off vs. on: noise floor rises from ~-60…-70 dB to ~-45…-50 dB, a measured ~15–20 dB SNR hit.

5. After down-conversion, before filtering — wanted band near 0 Hz, plus a residual image band (~±30–35 kHz) and LO-leakage spurs (~±16 kHz). This is exactly why a decimating LPF must follow a real quadrature mixer.

6. After the decimating LPF — Hamming window, 3.3 kHz cutoff, ÷22. Image and leakage removed; amplitude back to ~±15.

7. Ready for demodulation — recombined I/Q, exactly as fed into the OFDM Receiver; tracks Stage 2 closely, with AWGN as the main visible difference.

8. After demodulation — recovered byte stream. Amplitude range (~20–120) matches printable ASCII; the spectrum is a sharp, evenly-spaced harmonic comb — the expected Fourier signature of a looped, period-locked, essentially bit-exact recovery.

Noise Source amplitude was swept across 30 settings (0.5–6.5). Every sent message is one OFDM packet of 560 mapped symbols (~7 OFDM symbols, per the packet_len tag); sent packets were logged via a dedicated File Sink (data/TX_OFDM_Data_every_single_message.txt) and compared against data/RX_OFDM_Data.txt. Full analysis: analysis/visualize_results.ipynb.
| Noise | Sent | Received | PDR | Noise | Sent | Received | PDR |
|---|---|---|---|---|---|---|---|
| 0.5 | 4682 | 4680 | 99.96% | 4.2 | 6556 | 2183 | 33.30% |
| 2.7 | 4995 | 4989 | 99.88% | 4.3 | 7180 | 1926 | 26.82% |
| 3.0 | 4682 | 4627 | 98.83% | 4.4 | 10303 | 2133 | 20.70% |
| 3.1 | 5307 | 5177 | 97.55% | 4.5 | 6243 | 943 | 15.11% |
| 3.2 | 5307 | 5059 | 95.33% | 4.6 | 6243 | 670 | 10.73% |
| 3.3 | 6868 | 6301 | 91.74% | 4.7 | 6243 | 473 | 7.58% |
| 3.4 | 4995 | 4376 | 87.61% | 4.8 | 5931 | 287 | 4.84% |
| 3.5 | 5307 | 4271 | 80.48% | 4.9 | 5620 | 187 | 3.33% |
| 3.6 | 5931 | 4403 | 74.25% | 5.0 | 5619 | 126 | 2.24% |
| 3.7 | 5307 | 3574 | 67.33% | 5.1 | 5932 | 82 | 1.38% |
| 3.8 | 6556 | 3842 | 58.60% | 5.2 | 5619 | 46 | 0.82% |
| 3.9 | 5629 | 3057 | 54.31% | 5.3 | 7493 | 35 | 0.47% |
| 4.0 | 6556 | 2997 | 45.71% | 5.4 | 6243 | 22 | 0.35% |
| 4.1 | 8429 | 3417 | 40.55% | 5.5 | 5619 | 12 | 0.21% |
| 6.0 | 9990 | 2 | 0.02% | ||||
| 6.5 | 25600 | 0 | 0.00% |
Reading the curve:
- Classic waterfall shape: flat near 100% up to noise≈3.0, a sharp transition through 3.5–4.5, 50%-crossing at noise≈3.95, then a long tail to 0% by noise=6.5.
- The knee is sharp rather than gradual because CRC32 turns individual symbol errors into a hard pass/fail per packet, not a gradually-degrading one.
- Even in the extreme tail (2 of 9990 delivered at noise=6.0), every delivered packet is still byte-exact. This isn't a modulation effect — QPSK vs. BPSK affects whether a packet survives, not how corrupted a surviving one is. It's the CRC32 check itself: a packet either matches its checksum and is forwarded whole, or fails and is dropped whole — there's no partial-corruption state, so "delivered" and "byte-exact" are the same event at any noise level.
- Delivery isn't literally 100% even at the lowest tested noise (amplitude 0.5 → 99.96%, 2/4682 lost) — a reminder that not all loss here is SNR-driven. A small residual floor from synchronization/framing edge effects (e.g. correlator misses at packet boundaries) persists even at negligible noise, separate from the AWGN-driven mechanism that dominates the rest of the curve.
- Each point aggregates thousands of packets (n = 4682–25600); the binomial standard error stays under ~1% even near the 50% point (n≈5629 → SE≈0.7%), so the curve's shape reflects the underlying physics, not per-run measurement noise.
- This is exactly the behavior real systems are built around: link adaptation / AMC (802.11 rate control, LTE/5G CQI-driven MCS selection) exists to step down to a more robust modulation before a link falls off this cliff, not after.
- The x-axis is the linear
Amplitudeparameter of the GaussianNoise Source, not a calibrated SNR in dB — see Possible Improvements.
# 1. Install GNU Radio: https://wiki.gnuradio.org/index.php/InstallingGR
# 2. Open the flowgraph
gnuradio-companion flowgraphs/ofdm_baseband_passband_transceiver.grc
# 3. ...or run it directly
python3 flowgraphs/ofdm_baseband_passband_transceiver.py
# 4. Post-process results
jupyter notebook analysis/visualize_results.ipynbdata/TestData.txt is read (looped) as the payload source. data/TX_OFDM_Data_every_single_message.txt logs every packet sent; data/RX_OFDM_Data.txt logs what's recovered.
ofdm-phy-simulation/
├── analysis/
│ └── visualize_results.ipynb
├── data/
│ ├── TestData.txt
│ ├── TX_OFDM_Data_every_single_message.txt
│ └── RX_OFDM_Data.txt
├── flowgraphs/
│ ├── ofdm_baseband_passband_transceiver.grc
│ └── ofdm_baseband_passband_transceiver.py
├── images/
│ ├── Block_Diagram_of_the_Simulated_OFDM_Transceiver_System.jpg
│ ├── sine_carr.jpg
│ ├── after_OFDM_modulation.jpg
│ ├── after_OFDM_modulation_spec.jpg
│ ├── OFDM_modulation_on_carrier.jpg
│ ├── OFDM_modulation_on_carrier_spec.jpg
│ ├── OFDM_modulation_on_carrier_with_noise_spec.jpg
│ ├── OFDM_modulation_after_lowering_freq_spec.jpg
│ ├── OFDM_modulation_after_FIR_filter.jpg
│ ├── OFDM_redy_to_demodulation.jpg
│ ├── OFDM_redy_to_demodulation_spec.jpg
│ ├── after_OFDM_demodulation.jpg
│ ├── after_OFDM_demodulation_spec.jpg
│ └── packet_delivery_ratio.png
├── LICENSE
└── README.md
- Add forward error correction (convolutional/LDPC) on top of the existing CRC32 detect-and-drop scheme, and compare coded vs. uncoded waterfall curves — this project deliberately stops at error detection, not correction
- Calibrate the noise-amplitude x-axis into a proper Es/N0 (dB) scale — measure signal and noise power separately with a
Probe Avg Mag^2 Valueblock rather than comparing linear amplitudes - Rebuild the RX chain from primitive blocks (Sync, Channel Estimator, Equalizer, Serializer) to expose a constellation/EVM plot
- Channel estimation / equalization using the pilot subcarriers; timing and carrier frequency synchronization
- Multipath / fading channel model (Rayleigh, Rician) in addition to AWGN
- Run the same flowgraph over a real SDR (HackRF / RTL-SDR)
MIT — see LICENSE.

