FFT and Audio Visualizer on FPGA

Verilog Goertzel DFT XADC VGA Basys 3

Overview

A 256-bin audio spectrum analyzer built entirely from scratch in raw Verilog on a Basys3 FPGA. There is no CPU, no operating system, and no vendor FFT core anywhere in the design — every resonator, filter, and pixel on the VGA display is hand-written, verified the hard way: synthesize, implement, flash, and read the screen, with no simulator in the loop. It's part two of a series that started with Neural Network on an FPGA, same no-CPU, no-vendor-IP constraint, this time applied to real-time DSP instead of ML inference.

256 frequency bins, each its own Goertzel resonator
512 sample window per bin, over a real-valued input signal
~2ms full 256-bin sweep: 131,072 cycles at 65MHz
0 vendor FFT IP cores in the final design

Audio in

A generic electret-mic sensor module feeds the Basys3's XADC, which only accepts 0–1V on its analog input pins. The sensor's output swings well above that, so an external 10kΩ/10kΩ resistor divider attenuates it before it reaches JXADC pin 1, with the channel's N pin tied straight to ground since the XADC is a differential input. The result is a clean 12-bit, 0–4095 sample, exposed by a driver that knows nothing about filters, VGA, or the FFT downstream — just "here is the current level." audio_driver.v

Why Goertzel, not a radix-2 FFT

The first version of this pipeline used Xilinx's own FFT LogiCORE IP. Getting it running required a runtime scaling schedule pushed through an AXI-Stream config port, with no fixed, compile-time option exposed anywhere in the IP wizard. Several rounds of debugging output that looked scattered and random eventually traced back to an all-zero scaling schedule silently overflowing inside the core — a black box with no way to check the fix against a bit-accurate datasheet diagram, and no simulator on hand to step through it. The vendor IP was dropped for a hand-written, fully self-contained 128-point radix-2 FFT instead, with scaling made provably safe by construction: dividing every butterfly output by two, every stage, bounds the result so it can never overflow.

The final design goes one step further and drops the FFT butterfly network too, in favor of a bank of 256 Goertzel resonators. Both are legitimate ways to compute a DFT and give identical per-bin magnitudes for a fixed set of bins, but Goertzel's control logic is a plain nested loop — no bit-reversal addressing, no staged twiddle-factor indexing — which matters a lot when the only way to test a change is a full synthesize-implement-bitstream-hardware cycle, with nothing to simulate against locally. fft_view.v

The signal path

Analog audio in, a 1024×768 VGA frame out, three live views on screen at once.

01. Audio capture

XADC Wizard IP, single channel, continuous mode, unipolar, reading channel VAUX6. audio_driver.v

02. Complementary lowpass / highpass split

Two single-pole IIR filters run on the same audio-rate signal. The lowpass is a plain exponential moving average; the highpass reuses the exact same recurrence and just subtracts it from the input, since input = lowpass(input) + highpass(input) by construction — lower risk than inventing a second filter equation blind. Both cutoffs are live-adjustable from the board: a switch picks which filter, and two buttons step its cutoff up or down. lowpass_filter.v, highpass_filter.v

03. Waveform view

The raw trace and the band-passed trace are drawn side by side in the top half of the frame, so the effect of the live filter cutoffs is visible directly against the unfiltered signal. waveform_view.v

04. Spectrum view: 256 Goertzel resonators

Each of the 256 displayed bins runs its own Goertzel recurrence over a 512-sample window, computing power (magnitude squared, no square root needed) rather than a classic FFT butterfly pass. Bin 0 (DC) is skipped, so bins 1–256 cover the whole unique half of a real-valued input's Hermitian-symmetric spectrum. Power gets compressed with an approximate log2 before it becomes a bar height, so the display has log-like dynamic range instead of blocky power-of-two jumps. fft_view.v

05. Display composition

All three views tile onto one 1024×768@60Hz VGA frame: raw waveform top-left, filtered signal top-right, the 256-bin spectrum along the bottom, with a blue and a purple vertical marker tracking whichever filter's cutoff is currently being adjusted. top.v, vga_timing.v

The full bench setup: an LG monitor showing the live waveform and 256-bin spectrum with pink and blue cutoff markers, the Basys3 board wired to the sound sensor on a breadboard, and a laptop running a tone generator for testing
The actual bench mid-run — Basys3, sound sensor, and a tone generator for a known test signal.

Repository

Cleanly separated: the Vivado project, and the video explainer built alongside it.

  • FFTspectrum/: the Vivado project — every hand-written Verilog module (audio_driver, lowpass_filter, highpass_filter, waveform_view, fft_view, vga_timing, top) plus the generated XADC and clocking-wizard IP.
  • scenes/: the Manim scenes and narration script used to build the accompanying video, one folder per concept — sampling, Nyquist, filters, the DFT, the Goertzel algorithm, log compression.
  • scripts/: release automation for publishing the video build.

References

What this was built on top of.

← Back to Beyond Software