11 · FFT: Altera's IP vs our own
Can you use Altera's FFT IP on the DE10-Lite, and what does it cost compared with writing your own? Both run here on the same 256-point signal, and the PC checks both against numpy.
The repo keeps the full project (.qar, plus the sources here) but no prebuilt bitstream: one build (a few minutes) makes the .sof on your computer.
The FFT IP is a licensed Altera core. Quartus Prime Lite compiles it in OpenCore Plus evaluation mode: the
build makes fft_time_limited.sof instead of fft.sof, and the design runs only while the programmer stays
connected ("tethered": quartus_pgm keeps running) or for a limited time. Our own FFT has no such limit.
The result
| 256-point complex FFT, 16-bit | Altera FFT II IP Burst, 1 engine, block floating point | Our fft_r2radix-2, 1 butterfly, /2 every stage |
|---|---|---|
| Accuracy: signal-to-error vs numpy | 66.9 dB (max error 188) | 56.3 dB (max error 786) |
| Strongest bins found | 10, 37, 85 (the three tones) | 10, 37, 85 |
| Time for one FFT at 50 MHz | 1,637 clocks = 33 µs | 6,145 clocks = 123 µs |
| Logic cells | 1,698 | 398 |
| Memory bits (M9K blocks) | 9,472 (3) | 12,288 (3) |
| Multiplier elements (9-bit) | 8 | 8 |
| License | evaluation in Lite (time-limited .sof) | none |
Reading it: the IP is about 4 times bigger in logic, 4 times faster, and 10 dB more accurate (block floating point scales only when the numbers need it; our FFT halves at every stage and loses bits on a signal that never needed it). Our FFT's 56 dB is about 9 good bits on a 16-bit signal: plenty for a spectrum display, not for audio analysis. Both ran on the board; our FFT gives exactly the same numbers in simulation and on the board.
Block diagram
Using an IP from the library, without the GUI
Altera's IP cores live in C:\intelFPGA_lite\17.1\ip\altera. Each has a _hw.tcl file whose
add_parameter lines are the settings. ip-generate builds a configured copy into your project:
python examples/11_fft/ip/generate_fft_ip.py
→ ip-generate --component-name=altera_fft_ii --output-name=fft256 --file-set=QUARTUS_SYNTH
--component-parameter=length=256 --component-parameter=data_flow=Burst ... → ip/fft256/fft256.qip
Add the .qip to the project (src/extra.qsf: set_global_assignment -name QIP_FILE ip/fft256/fft256.qip)
and instantiate fft256. When a setting is not allowed for the chosen architecture, ip-generate prints an
Error line saying why (here: Burst needs Bi-directional and Natural output order).
Talking to the FFT IP
- In: one sample per clock while
sink_readyis high:sink_valid,sink_sopon the first sample,sink_eopon the 256th,sink_real/sink_imag.inverse= 0 for the forward FFT. - Out:
source_validwithsource_sop/source_eoparound 256 results in natural order, andsource_exp, the block exponent. - Scale: the true FFT value = output × 2−exp. Here exp = −7, so ×128. (Checked: with that scale the error against numpy is 66.9 dB below the signal.)
Our FFT in brief
src/fft_r2.sv: the 256 samples are written into one RAM at bit-reversed addresses; then 8 stages × 128
butterflies, each reading two values and a twiddle (cos, sin in Q15 from assets/twiddle.hex), multiplying with 4
multipliers, and writing back (a + W·b)/2 and (a − W·b)/2. Halving every stage makes overflow impossible; the price is
precision. Ideas to make it better: halve only when a stage would overflow (block floating point), or two butterflies in
parallel with a second RAM.
Bugs met on the way (each now a gotcha)
- An unsized number in a concatenation.
{sign, 8'h30 + (m / 10), ...}: the bare10is 32 bits, so each term became 32 bits and the text came out as zero bytes. Size everything inside{ }. - The later assignment wins.
if (go) t <= 0; ... if (run) t <= t + 1;in one block never reset the counter. - A third read port kills block RAM. Reading
ram[cnt]directly (not through a clocked port) would have turned the RAM into 8,192 flip-flops.
Run it
python scripts/de10.py build examples/11_fft
python scripts/de10.py program examples/11_fft (keeps running: tethered; stop it with Ctrl+C)
python scripts/de10.py send R 8 > logs/fft_dump.txt (in a second terminal)
python examples/11_fft/sim/fft_data.py check logs/fft_dump.txt
Build it yourself
python examples/11_fft/sim/fft_data.py make # input.hex, twiddle.hex
python examples/11_fft/ip/generate_fft_ip.py # the IP
python scripts/new_project.py examples/11_fft fft_top --lib print seg7
python scripts/de10.py build examples/11_fft
Test without the board
python scripts/sim.py examples/11_fft/sim/tb_fft_r2.sv
python examples/11_fft/sim/fft_data.py check examples/11_fft/sim/diy_dump.txt → DIY 56.3 dB, bins 10 37 85
Files
| File | What |
|---|---|
src/fft_top.sv | Feeds both FFTs, collects, sends to the PC |
src/fft_r2.sv | Our radix-2 FFT |
ip/generate_fft_ip.py, ip/fft256/ | The IP generation command and its output |
sim/fft_data.py | Test signal, twiddles, accuracy check against numpy |
sim/tb_fft_r2.sv | Unit test of our FFT |