An OFDM modem prototype on the PYNQ-Z2 (Zynq-7020), built to learn the Wi-Fi PHY layer hands-on. Inspired by the openwifi project from IDLab/imec at Ghent.
A full vertical slice of an OFDM modem on a single Zynq SoC.
On the FPGA side (PL): a 64-point OFDM transmit chain (scramble, QPSK map, IFFT, cyclic prefix) feeding a digital loopback into a mirror receive chain (CP remove, FFT, QPSK demap, descramble). All 12 stages are AXI-Stream connected, all custom Verilog except the FFT/IFFT (Vivado IP). No RF; the loopback is internal to the fabric.
- Per-block HDL verification: I did not write proper testbenches for each block. The end-to-end test on the board was the verification.
On the Linux side (PS): a userspace C program (mini_wifi_test.c)
allocates DMA buffers, loads them with test bytes, kicks the AXI
DMA registers via /dev/mem, waits for completion, and reads the
bytes that came out of the chain. DMA buffer allocation uses the
open-source u-dma-buf kernel module so I didn't have to write a
full kernel driver.
The two halves talk over Zynq's AXI interconnect: AXI-Lite for control registers, AXI4 Full for DMA between Linux DDR and the chain, AXI-Stream for the data path between Verilog blocks.
This is a miniature OFDM transmitter and receiver running through a digital loopback inside the FPGA fabric. OFDM is the modulation used by Wi-Fi, LTE, and 5G — bits are spread across many narrow subcarriers in parallel, which makes the signal robust to multipath. I wanted to understand it by building one.
It is not a full Wi-Fi PHY. Real 802.11 also includes forward error correction, a known preamble for synchronization, channel equalization, and a full MAC layer above it. I built only the core OFDM datapath — scramble, QPSK, IFFT, cyclic prefix on the TX side, and the symmetric inverse on RX — to see what the bare modulation chain looks like in HDL.
Scrambler. XORs bits with a 7-bit LFSR sequence (polynomial x^7 + x^4 + 1). Without this, all-zero data has no transitions, which breaks clock recovery at the receiver. XOR is its own inverse, so the descrambler is the same module.
QPSK mapper. Each pair of scrambled bits becomes a complex
point on the unit circle: 00 → (+1,+1), 01 → (-1,+1), etc.
This is the modulation step — bits become amplitude and phase.
64-point IFFT. Treats 64 QPSK symbols as 64 narrow subcarriers and sums them into one time-domain waveform. This parallel-narrowband structure is what makes OFDM tolerate multipath: each subcarrier is slow enough that echoes only blur the edges of each symbol, not the whole thing.
Cyclic prefix insert. Copies the last 16 samples of each 64-sample OFDM symbol onto the front, producing an 80-sample symbol. The CP is a guard region that absorbs echoes from the previous symbol so they do not corrupt the FFT window at the receiver.
Loopback. In a real radio this is where the samples would go through a DAC, RF mixer, antenna, the air, antenna again, ADC. Here they stay digital and feed the receive chain directly. No noise, no channel impairments.
Cyclic prefix remove + 64-point FFT. Strip the 16-sample CP, then FFT to recover the 64 subcarriers' QPSK symbols.
QPSK demapper + bit-pair unpack + descrambler. Sign of each complex number recovers the 2 bits; descrambling with the same LFSR recovers the original data.
The full chain runs on the board. Linux sends bytes through the TX side, the OFDM pipeline processes them, the RX side decodes back to bytes. DMA confirms the round-trip in both directions.
The output is not bit-perfect, and transfers above ~256 bytes stall, both because of a framing issue on the RX path I didn't have time to fix.
Built and tested on PYNQ-Z2 (Zynq-7020, xc7z020clg400-1).
In principle the design is portable to any Zynq-7000 or Zynq UltraScale+ board with an AXI Interconnect — you would need to re-target the Vivado project to the new part and regenerate the bitstream. There's nothing PYNQ-Z2 specific in the HDL itself.
- Flash the PYNQ-Z2 v3.0 SD image, boot, SSH in.
- Get internet to the board (ICS or router).
- Install build tools:
sudo apt install -y gcc-12 build-essential nano busybox-static cd /lib/modules/$(uname -r)/build sudo make scripts && sudo make modules_prepare - Build u-dma-buf:
cd ~ && git clone https://github.com/ikwzm/udmabuf cd udmabuf && make CC=gcc-12 KCFLAGS="-fno-stack-protector" sudo insmod u-dma-buf.ko udmabuf0=1048576 udmabuf1=1048576 - Copy
vivado/outputs/mini_wifi_chain.bitto the board, convert to the kernel-friendly.binformat, load via FPGA Manager. - Build and run the test:
gcc -O2 -o mini_wifi_test mini_wifi_test.c sudo ./mini_wifi_test 256
hdl/ custom Verilog blocks
vivado/ Tcl to recreate the project, packaged AXI-Lite IP, bitstream
linux/ hello-world kernel module + userspace test program
docs/ architecture diagram, demo screenshot, demo output
sim/ scrambler testbench scaffolding