BER-vs-Eb/N0 for orthogonal frequency-division multiplexing — the multicarrier waveform behind
Wi-Fi and 4G/5G — on the two-node COSMOS framework. OFDM is the payoff of the modulation arc: the
frequency-selective (multipath) channel that broke single-carrier 64-QAM on the grid open-air
link becomes, per subcarrier, a flat one-tap channel the receiver trivially equalizes.
OFDM sends data on N orthogonal subcarriers at once. Bits map to per-subcarrier symbols
(BPSK / QPSK / 16-QAM / 256-QAM), an IFFT turns them into a time-domain block, and a cyclic prefix (CP)
longer than the channel's delay spread is prepended so multipath cannot cause inter-symbol
interference. At the receiver an FFT returns to the subcarriers, each of which now sees only a
flat complex gain — removed with a single-tap equalizer.
Because the IFFT/FFT are unitary, OFDM's AWGN BER equals its subcarrier modulation's AWGN BER at
the same per-bit Eb/N0; the CP and pilots cost spectral efficiency, not Eb/N0. The teaching payoff
is on a multipath channel: where a single carrier's dense constellation smears, OFDM turns one hard
wideband channel into many easy flat narrowband ones.
Read the shared framework overview and the
PSK / QAM
pages first. This page covers what is specific to OFDM.
Source (clone & run): https://gitlab.orbit-lab.org/tutorials/ber
After completing this tutorial you will be able to:
| Difficulty | Advanced |
| Estimated time | 45 min |
| Domain / sandbox | any two-B210 domain; grid open-air is the multipath showcase |
| Topic group | SDR & GNU Radio |
| Last verified | 2026-07-20 — real grid OFDM-QPSK measured (13.2 dB, BER 0; locks on the multipath where single-carrier 16-QAM died at −6 dB); BPSK/QPSK/16/64/256-QAM subcarriers sim-validated vs theory |
Background knowledge
Account & access
Devices / nodes
| Resource | Role | Qty | Notes |
|---|---|---|---|
nodes[0] |
transmitter | 1 | USRP B210 |
nodes[1] |
receiver | 1 | USRP B210 |
Disk images
| Image | Load onto | Provides |
|---|---|---|
ubuntu2404-uhd4.9-gr3.10.ndz |
both nodes | UHD 4.9 + GNU Radio 3.10 + numpy (the OFDM modem is numpy) |
Software components
| Component | Version | Source |
|---|---|---|
| GNU Radio | 3.10 | preinstalled in image (USRP TX/RX only; the OFDM modem is numpy ofdmlib.py) |
| cosmos-ansible bundle | — | git clone https://gitlab.orbit-lab.org/tutorials/ber on the console |
Spectrum / RF / special
The OFDM parameters are teaching defaults: N = 64 subcarriers, CP = 16, 52 active
subcarriers (DC + a Nyquist guard band nulled), comb pilots, ~43 data subcarriers.
Two B210 nodes driven from the console. The TX precomputes the known OFDM waveform (IFFT + CP)
and plays it; the RX captures raw IQ and does the OFDM demod in numpy (FFT → data-aided
per-subcarrier channel estimate → zero-forcing equalize → BER against the known reference) — the
same precompute-and-play / capture-and-post-process pattern as the single-carrier families. The RF
path (open-air grid, cabled sb5, sb4 matrix) is per the
framework topology.
ssh <username>@console.<domain>.cosmos-lab.org
omf load -i ubuntu2404-uhd4.9-gr3.10.ndz -t <node0>,<node1>
omf tell -a on -t <node0>,<node1>
git clone https://gitlab.orbit-lab.org/tutorials/ber.git
cd ber
ofdm_ber.py is the pure-numpy simulator (ofdmlib modem): it proves the OFDM decode tracks the
closed-form theory, and — with a multipath channel — that OFDM keeps decoding where a single carrier
would smear:
python3 ofdm_ber.py --sub qpsk --ebn0 0:12:1 --nbits 3000000 # AWGN, tracks QPSK theory
python3 ofdm_ber.py --sub 16qam --ebn0 0:15:1 --nbits 4000000 # higher-order subcarriers
python3 ofdm_ber.py --sub qpsk --ebn0 4:16:1 --taps 1,0,0,0.5 # multipath (survives)
Transmit the known OFDM frame on nodes[0], capture raw IQ + demodulate on nodes[1]. OFDM is
CFO-sensitive, so the grid run shares a 10 MHz + PPS OctoClock (--clock-source external); the TX
wants a high gain (see the PSK page's operating-point note):
# TX (held-open ssh; grid OTA operating point, OctoClock):
ssh -f -n root@<node0> "setsid python3 ofdm_tx.py --sub qpsk --freq 2e9 --gain 89 \
--clock-source external --time-source external --args serial=<txser> </dev/null >/tmp/tx.log 2>&1"
# RX: capture + demod, then trace the waterfall by injecting calibrated AWGN on the clean capture:
ssh root@<node1> "python3 ofdm_rx.py --sub qpsk --freq 2e9 --gain 60 \
--clock-source external --time-source external --args serial=<rxser> --nsamp 4000000 \
--inject 0:12:1 --tx-node <node0> --rx-node <node1> --tx-gain 89 \
--sigmf results/ofdm_qpsk_grid.sigmf-meta"
For the real TX-power sweep, re-run the RX with no --inject once per TX gain (each capture
reports the genie real-link Eb/N0 and BER — one real operating point).
python3 sdr_plot.py results/ # -> results/ber_ofdm.svg
sdr_plot.py carries the OFDM theory curve (an ofdm-<sub> curve equals the <sub> curve).
Measured over the air on grid — TX node18-19 (s/n 3073463) @ gain 89, RX node18-2 (s/n 3073473),
2 GHz, with a shared OctoClock 10 MHz + PPS reference (ref_locked=True on both radios). The
receiver holds a solid lock (real-link Eb/N0 = 13 dB, BER 0), and the BER-vs-Eb/N0 curve is traced by
AWGN injection on that real capture at 0.2-dB steps (5 points/dB, ~5.8 M bits/point):
| Eb/N0 (dB) | 0 | 4 | 8 | 10 | 12 |
|---|---|---|---|---|---|
| OFDM-QPSK measured | 8.7e-2 | 1.9e-2 | 1.1e-3 | 1.4e-4 | 1.4e-5 |
| QPSK theory | 7.9e-2 | 1.25e-2 | 1.9e-4 | 3.9e-6 | — |
Theory vs real radios. OFDM's AWGN BER equals its subcarrier modulation's, so the theory line is
exactly the QPSK curve. The measured curve rides it and sits ~2–3 dB to the right — the honest
cost of a real multicarrier radio over the ideal channel: B210 EVM and residual timing/phase, plus
the OFDM-specific channel-estimation and pilot-tracking overhead.
OFDM needs frequency sync. OFDM is far more sensitive to carrier-frequency offset than a single
carrier: a CFO rotates the subcarriers into each other (inter-carrier interference), so free-running
B210 clocks give BER ≈ 0.5 even when frame sync locks. The shared OctoClock reference removes it
(BER 0). A CFO estimator would do the same on a domain without a shared clock.
omf tell -a offh -t <node0>,<node1>
| Symptom | Likely cause | Fix |
|---|---|---|
RX sync_conf low, BER ≈ 0.5 |
frame sync didn't lock onto the OFDM waveform | raise TX gain (grid wants ~89); confirm tx.log shows the TX line; capture more --nframes |
| OFDM decodes but curve floors early | too few captured periods for the low-BER tail | raise --nframes (or --nbits in the sim) |
| Sim BER ~2× theory | comb-pilot (not data-aided) channel estimation — its estimation-noise penalty | use the data-aided path (default); comb-pilots are the realistic-alternative demo |
No UHD Devices Found |
node ↔ B210 USB not enumerated | power-cycle the node; check uhd_find_devices |
Author(s): COSMOS team · Last verified: 2026-07-20 (real grid OFDM-QPSK 13.2 dB BER 0 + OTA noise-injection waterfall + TX-power sweep; BPSK/QPSK/16/64/256-QAM subcarriers sim-validated vs theory) · Tested image/release: ubuntu2404-uhd4.9-gr3.10.ndz / GNU Radio 3.10.12 · Tags: sdr, gnuradio, ber, ofdm, multicarrier, sigmf