FPGA Simulation and Testbenches¶
Simulation is the fastest way to develop and debug openwifi's FPGA logic without a board. You feed a testbench a recorded or generated IQ file, run it in Vivado (or Icarus Verilog), and inspect the receiver's internal signals in the waveform view or in the text files the testbench dumps. Every design change can be checked here before you spend an hour synthesizing a bitstream.
This page is the deep reference for that environment. For the surrounding build, deploy, and port workflow, see FPGA Development. For what each core does, see FPGA IP Cores.
Two kinds of testbench¶
openwifi's HDL ships two tiers of testbench. Reach for whichever matches the change you are making.
| Tier | Testbench | Where | Drives | Good for |
|---|---|---|---|---|
| Full receiver | dot11_tb |
openofdm/verilog/ |
The whole dot11 receive chain from an IQ file |
Sync, channel estimation, equalization, demod, Viterbi, FCS |
| Full transmitter | dot11_tx_tb |
openwifi-hw/ip/openofdm_tx/src/ |
openofdm_tx from a TX memory image |
Encoding, modulation, IFFT, preamble insertion |
| Block-level unit test | adc_intf_tb, mv_avg_tb, fifo_sample_delay_tb |
openwifi-hw/ip/<core>/unit_test/<block>/ |
One submodule with hand-written or file-based stimulus | A single register, FIFO, or math block in isolation |
The full-chain benches are the ones you spend most time in. The block-level unit tests are small and fast, and they exist only for the handful of blocks that are worth checking on their own (see the IP Cores table).
The openofdm_rx receiver testbench (dot11_tb)¶
This is the main simulation environment. dot11_tb instantiates the dot11 receiver, streams an IQ file into it one sample at a time, and records what comes out. Because openofdm_rx is a git submodule, first pull it in (see FPGA Development):
./get_ip_openofdm_rx.sh
Running it in Vivado¶
- Create the IP's Vivado project:
cd ip/openofdm_rx
../create_vivado_proj.sh $XILINX_DIR openofdm_rx.tcl
- In Vivado, open Sources → Simulation Sources → sim_1 →
dot11_tb. - Run SIMULATION → Run Simulation → Run Behavioral Simulation. The first run is slow because every sub-IP compiles once. Later runs are fast.
- Press Run All (F3) to run to completion.
- After editing a design file, use Relaunch Simulation rather than recreating the project.
A passing run ends with one line per frame in fcs_out.txt (the sample count followed by the fcs_ok flag), and byte_out.txt holds the decoded bytes to compare against the known frame. The first run can take several minutes while every sub-IP compiles.
Icarus Verilog as a lighter alternative
The openofdm repo also builds with Icarus Verilog and GtkWave instead of Vivado. A Makefile lives in openofdm/verilog/: make compiles the design and runs dot11_tb under vvp, and make clean removes the outputs. This is handy on a machine without a Vivado install, though the Xilinx primitives (the Viterbi decoder) still need the Vivado simulation libraries, which you generate with Vivado's compile_simlib.
The IQ input: what you feed¶
The receiver expects 20 MSPS baseband IQ, 16 bits each. The testbench reads its samples from a text file named by the `SAMPLE_FILE macro in verilog/openofdm_rx_pre_def.v. Change that macro to point at a different capture, then relaunch.
Each line of the file is three integers: I, Q, and a dummy RSSI value.
-152 37 0
-4 -211 0
88 -19 0
dot11_tb reads one line per 20 MHz tick, packs it into sample_in[31:16] (I) and sample_in[15:0] (Q), and pulses sample_in_strobe for one cycle. The baseband clock is selectable in the testbench (CLK_SPEED_100M, 200M, 240M, 400M), but the sample rate stays 20 MSPS regardless: the harness just spaces the strobes further apart at a higher clock.
dot11_tb data flow. A recorded or generated IQ file drives the receiver one sample at a time. Results go two ways at once: the waveform view for interactive debugging, and dumped text files that a Python reference decoder can check against.What to watch on the outputs¶
dot11_tb monitors the receiver's full progress. Drag these signals from SIMULATION → Scope into the waveform view (for example dot11_tb → dot11_inst → ofdm_decoder_inst → viterbi_inst for the decoder). Together they show exactly how far a packet got and where it failed.
| Signal | What it means |
|---|---|
short_preamble_detected, long_preamble_detected |
Packet detection and coarse/fine timing found the preamble. If these never fire, the problem is sync or signal level, not decoding. |
state |
The receiver FSM position. Watch it step through detection, channel estimation, SIGNAL parsing, and payload demod. A stuck state localizes the fault to one stage. |
pkt_header_valid, pkt_len, pkt_rate |
The SIGNAL/HT-SIG field parsed and gave a length and rate. A wrong pkt_len here means SIGNAL decoding, not payload decoding, is off. |
demod_is_ongoing |
Payload demodulation is running. |
byte_out, byte_out_strobe |
The decoded MPDU bytes, one at a time. Compare against the known frame. |
fcs_ok, fcs_out_strobe |
The CRC check passed. This is the end-to-end pass/fail for the whole receive chain. |
Alongside the waveform, the testbench uses $fopen/$fscanf/$fwrite to dump many intermediate results as text, so you can check them numerically rather than by eye. The files land in the simulation working directory (openofdm_rx/openofdm_rx.sim/sim_1/behav/xsim/).
| Dumped file | Contents |
|---|---|
short_preamble_detected.txt, sync_long_out.txt |
Preamble-detection and long-training-field sync results |
equalizer_out.txt |
Per-subcarrier equalizer output (the constellation) |
demod_out.txt, demod_soft_bits.txt |
Demodulated symbols and the soft bits fed to Viterbi |
byte_out.txt, descramble_out.txt, fcs_out.txt |
Decoded bytes, descrambler output, and the final CRC result |
status_code.txt, phy_len.txt |
Receiver status codes and the parsed PHY length |
Cross-checking against the Python decoder¶
openofdm ships a bit-exact Python model of the receiver under scripts/ (it uses commpy for the convolutional code). Running the same IQ file through the Python decoder and diffing its output against the Verilog dumps is how you confirm the hardware matches the reference. When a stage diverges, the first file that disagrees points straight at the module that changed behavior.
The test vectors¶
Sample IQ files live in openofdm/testing_inputs/, grouped by how they were produced. Pick the group that matches what you are testing.
| Directory | Origin | Use it to |
|---|---|---|
simulated/ |
Generated with an ideal (no-noise) channel: ag_*.txt (11a/g), ht_mcs*_gi*.txt (11n), iq_*.txt |
Verify decoding logic against a clean, known-good signal |
conducted/ |
Captured over a cable (dot11a_*, dot11n_* at each rate) |
Test against a real but low-distortion signal |
radiated/ |
Captured over the air, with .pcap companions |
Test sync and equalization against real multipath and noise |
File names encode the format and rate, for example dot11n_6.5mbps or ht_mcs7_gi1. Start from a simulated/ file at a low MCS when bringing up a change, then move to conducted/ and radiated/ to stress synchronization and the equalizer. Note that the scripts/ Python model only covers the receiver direction (it decodes a sample file). The repo ships no generator for producing a fresh simulated/ vector.
Automated and batch simulation¶
For regression-style runs you do not need the GUI. The openofdm repo has three helper Tcl scripts at its root that drive XSim in batch mode:
| Script | What it does |
|---|---|
openofdm_rx_sim_iq_file.tcl |
Runs dot11_tb against one IQ file passed as an argument |
openofdm_rx_sim_iq_file_batch.tcl |
Loops the single-file run over many IQ files |
openofdm_rx_side_ch_sim_ultra_scale.tcl |
Simulates the receiver together with side_ch on UltraScale parts |
openofdm_rx_sim_iq_file.tcl shows the pattern the others follow. It writes the chosen file into the `SAMPLE_FILE macro, computes the run length from the file (lines / 20 microseconds by Tcl integer division, since the input is 20 MSPS), runs the simulation, and copies every dumped .txt into a results directory named after the input file. That last step is what makes batch runs comparable: each input keeps its own set of dumps.
The transmitter testbench (dot11_tx_tb)¶
The transmit side has its own testbench, dot11_tx_tb.v, which drives openofdm_tx from a memory image instead of an IQ file. The test vectors are .mem files under ip/openofdm_tx/unit_test/test_vec/, named for the frame they encode:
tx_intf.mem: the base TX interface memory image.ht_tx_intf_mem_mcs7_gi1_aggr0_byte100.memand..._byte8176.mem: 802.11n HT frames at MCS7, short guard interval, for a 100-byte and an 8176-byte payload.
Simulate it the same way as the receiver: create the openofdm_tx project, select dot11_tx_tb as the simulation top, and run behavioral simulation. The testbench writes each output sample as one I Q line to dot11_tx.txt. Because the transmitter is deterministic, that output can be converted into a dot11_tb sample file (append the dummy RSSI column the receiver format expects) and replayed through the receiver. This is the self-loopback test you run on hardware, done entirely in simulation.
Block-level unit tests¶
A few submodules have their own tiny testbench so you can verify them without building a full core. Each lives in ip/<core>/unit_test/<block>/ with a <block>_tb.v and a <block>_tb.tcl. The Tcl script builds a standalone Vivado project (targeting the ZCU102 part xczu9eg-ffvb1156-2-e) that contains only that block and its testbench, with a short preset run time.
| Unit test | Block under test | Stimulus |
|---|---|---|
ip/rx_intf/unit_test/adc_intf |
adc_intf.v, the ADC-side sample unpacker |
Generated inside adc_intf_tb.v |
ip/xpu/unit_test/mv_avg |
mv_avg.v / mv_avg_dual_ch.v, the RSSI moving-average filter |
test_vec/data_in.txt, with test_data_in_out.m as the MATLAB reference |
ip/xpu/unit_test/fifo_sample_delay |
fifo_sample_delay.v, the sample-delay FIFO |
Generated inside fifo_sample_delay_tb.v |
To run one, source its .tcl to create the project, then open it and run behavioral simulation:
cd ip/xpu/unit_test/mv_avg
vivado -mode batch -source mv_avg_tb.tcl
# the batch run only creates the ./mv_avg_tb project, it does not simulate
# open ./mv_avg_tb in the Vivado GUI and Run Behavioral Simulation
The mv_avg test is the clearest example of the file-vector pattern: test_data_in_out.m generates data_in.txt and the expected output in MATLAB, the testbench reads data_in.txt, and you compare the Verilog result against the MATLAB reference. That is the same read-a-vector, dump-a-result, diff-against-a-golden-model loop the full dot11_tb uses, shrunk to one block.
Conditional compilation in simulation¶
create_vivado_proj.sh passes extra arguments through to `define macros in <ip_name>_pre_def.v, the same mechanism the full build uses. In simulation these macros select the sample file, the clock speed, and debug blocks. See FPGA Development → conditional compilation for the argument order, and remember to pass the same macros to create_ip_repo.sh when you later build the top-level project, so the synthesized core matches what you simulated.
From simulation to hardware¶
Simulation and on-board debugging cover different failures. Use the testbench to verify logic and datapath correctness against known vectors, where you have full visibility and a golden reference. Use the Xilinx ILA on the running board to catch the things simulation cannot show: real RF, real timing against the AD9361, and the interaction between the FPGA and the Linux driver. A common workflow is to reproduce a hardware bug in simulation by capturing the offending IQ with side_ch (see Research Features), saving it in the dot11_tb sample-file format, and replaying it through the receiver testbench.