Real-Time FPGA Spectrum Analyzer
A live spectrum analyzer implemented in Verilog on a Sipeed Tang Nano 9K. An external SPI ADC samples an analog signal, the FPGA computes its frequency content, and the spectrum renders to a display — all as parallel digital logic, with no processor in the signal path. The endgame is to feed the radar gun's amplifier output into this board: analog RF front end → ADC → FPGA DSP → display, the literal architecture of a production radar processor at hobby scale.
The function of a spectrum analyzer is solved — Audacity does it. The implementation is the point: clock domain crossing, hard real-time pixel timing, fixed-point pipelined math, wire-level protocol implementation from first principles. Every module has a self-checking Icarus Verilog testbench and, where applicable, a Python / NumPy fixed-point golden model the RTL is verified against. Nothing touches the board until its testbench passes.
The signal chain
Two clock domains — sampling (~20 kHz) and pixel (25 MHz) — bridged by a gray-code async FIFO. That FIFO is the single most important structural decision in the project.
Module status
Every module verified in simulation against a self-checking testbench — and where DSP is involved, against a NumPy fixed-point golden model on identical input vectors, so the RTL output matches the reference to within quantization error.
| Module | Purpose | Simulation | Hardware |
|---|---|---|---|
| uart_tx.v | 8N1 UART transmitter, FSM | ✓ Verified | Pending |
| uart_rx.v | UART receiver, mid-bit sampling | ✓ Verified | Pending |
| spi_mcp3008.v | SPI master, two-edge discipline | ✓ Verified | Pending |
| synchronizer.v | 2-flop metastability synchronizer | ✓ Verified | Pending |
| async_fifo.v | Gray-code dual-clock FIFO (BRAM) | ✓ Verified | Pending |
| vga_timing.v | 640×480 @ 60 Hz timing generator | ✓ Verified | Pending |
| test_pattern.v | 8-bar color test pattern | ✓ Verified | Pending |
| goertzel_bin.v | Fixed-point Goertzel per-bin | In progress | — |
| fft_pipelined.v | 256-pt radix-2 pipelined FFT | Planned | — |
| tmds_serializer.v | HDMI/DVI output stage | Hardware only | — |
The FIFO is the whole project
The ADC samples in one clock domain (~20 kHz), the display runs in another (25 MHz pixel clock). Wiring data across raw risks metastability: a flip-flop sampling a signal mid-change gets stuck between 0 and 1, corrupting data randomly. Two mechanisms make this safe:
2-flop synchronizer
- Two flip-flops in series in the destination domain. If the first goes metastable, it settles before the second samples it.
- Clean output, one cycle late. For single-bit signals only.
Gray-code async FIFO
- The real bridge for multi-bit data. Shared memory, separate read/write pointers.
- Pointers cross in gray code: only one bit changes at a time, so a mid-transition read is at worst off by that one bit.
- Then synchronized through the 2-flop synchronizer. 40 values crossed between two different clocks in order, uncorrupted.
What each module taught me
Bench-verified before the FPGA
Confirmed the MCP3008 + MAX4466 chain without the FPGA in the loop: 3.3 V rail steady, VDD/VREF referenced, mic output biased mid-rail (~1.65 V), signal present at ADC CH0 (meter wobble on sound). SPI pins waiting for FPGA bring-up. This is the same debugging discipline used on the radar gun: isolate every stage, measure at every node, and don't power the next thing until the previous one measures right.
What's next
DSP · Stage 1
- 16-band Goertzel filterbank in fixed-point Q1.15, MAC arithmetic time-shared across bins.
- An honest, complete spectrum analyzer on its own — renders as 16 bars.
- Verified against a NumPy fixed-point reference on identical input vectors.
DSP · Stage 2
- Pipelined 256-point radix-2 FFT: butterfly unit, twiddle ROM, bit-reversed addressing, alpha-max + beta-min magnitude approximation.
- Upgrade rendering to a scrolling waterfall (spectrogram) stored in BRAM.
- Simulation output has to match NumPy's FFT within fixed-point quantization error.
Hardware bring-up
- TMDS serializer + rPLL for the pixel clock on the Tang Nano's HDMI connector.
- Live oscilloscope trace of the mic signal first — whistle and watch the sine wave.
- Timing closure report + resource utilization documented in the repo.
Radar integration
- Feed the Doppler radar's amplifier output into the ADC (already biased near mid-rail — convenient).
- Calibrated axis: bin → Hz → mph at 31.4 Hz/mph.
- Final demo: point at a passing car, the spike sits at its speed — all-hardware DSP.