Implement iterative single-butterfly FFT engine (fft_engine.v) supporting 1024-pt and 32-pt transforms with quarter-wave twiddle ROM, XPM_MEMORY_TDPRAM for guaranteed BRAM mapping in Vivado, and behavioral model for simulation. Add xfft_32.v AXI-Stream wrapper for doppler_processor integration and dual-branch matched_filter_processing_chain.v (behavioral + synthesis paths). Fix placement failure caused by 68K+ registers from dissolved memory arrays: - doppler_processor.v: extract mem writes to sync-only always block for BRAM - xfft_32.v: extract buffer writes to sync-only always block for LUTRAM Post-implementation: 37K regs (29%), 23K LUTs (37%), 10 BRAM (7%), fully routed. All testbenches pass: fft_engine 12/12, xfft_32 10/10, mf_chain 27/27.
527 lines
16 KiB
Verilog
527 lines
16 KiB
Verilog
`timescale 1ns / 1ps
|
|
|
|
/**
|
|
* tb_fft_engine.v
|
|
*
|
|
* Testbench for the synthesizable FFT engine.
|
|
* Tests with N=32 first (fast), then validates key properties.
|
|
*
|
|
* Test Groups:
|
|
* 1. Impulse response: FFT of delta[0] should be all 1s
|
|
* 2. DC input: FFT of all-1s should be delta at bin 0
|
|
* 3. Single tone: FFT of cos(2*pi*k/N) should peak at bin k
|
|
* 4. Roundtrip: FFT then IFFT should recover original
|
|
* 5. Linearity: FFT(a+b) ~= FFT(a) + FFT(b)
|
|
*
|
|
* Convention: standard check task with pass/fail tracking.
|
|
*/
|
|
|
|
module tb_fft_engine;
|
|
|
|
// ============================================================================
|
|
// PARAMETERS — test with 32-pt for speed
|
|
// ============================================================================
|
|
localparam N = 32;
|
|
localparam LOG2N = 5;
|
|
localparam DATA_W = 16;
|
|
localparam INT_W = 32;
|
|
localparam TW_W = 16;
|
|
localparam CLK_PERIOD = 10;
|
|
|
|
// ============================================================================
|
|
// SIGNALS
|
|
// ============================================================================
|
|
reg clk, reset_n;
|
|
reg start, inverse;
|
|
reg signed [DATA_W-1:0] din_re, din_im;
|
|
reg din_valid;
|
|
wire signed [DATA_W-1:0] dout_re, dout_im;
|
|
wire dout_valid, busy, done_sig;
|
|
|
|
// ============================================================================
|
|
// DUT
|
|
// ============================================================================
|
|
fft_engine #(
|
|
.N(N),
|
|
.LOG2N(LOG2N),
|
|
.DATA_W(DATA_W),
|
|
.INTERNAL_W(INT_W),
|
|
.TWIDDLE_W(TW_W),
|
|
.TWIDDLE_FILE("fft_twiddle_32.mem")
|
|
) dut (
|
|
.clk(clk),
|
|
.reset_n(reset_n),
|
|
.start(start),
|
|
.inverse(inverse),
|
|
.din_re(din_re),
|
|
.din_im(din_im),
|
|
.din_valid(din_valid),
|
|
.dout_re(dout_re),
|
|
.dout_im(dout_im),
|
|
.dout_valid(dout_valid),
|
|
.busy(busy),
|
|
.done(done_sig)
|
|
);
|
|
|
|
// ============================================================================
|
|
// CLOCK
|
|
// ============================================================================
|
|
initial clk = 0;
|
|
always #(CLK_PERIOD/2) clk = ~clk;
|
|
|
|
// ============================================================================
|
|
// PASS/FAIL TRACKING
|
|
// ============================================================================
|
|
integer pass_count, fail_count;
|
|
|
|
task check;
|
|
input cond;
|
|
input [512*8-1:0] label;
|
|
begin
|
|
if (cond) begin
|
|
$display(" [PASS] %0s", label);
|
|
pass_count = pass_count + 1;
|
|
end else begin
|
|
$display(" [FAIL] %0s", label);
|
|
fail_count = fail_count + 1;
|
|
end
|
|
end
|
|
endtask
|
|
|
|
// ============================================================================
|
|
// STORAGE FOR CAPTURED OUTPUTS
|
|
// ============================================================================
|
|
reg signed [DATA_W-1:0] out_re [0:N-1];
|
|
reg signed [DATA_W-1:0] out_im [0:N-1];
|
|
integer out_idx;
|
|
|
|
// Second set for roundtrip
|
|
reg signed [DATA_W-1:0] out2_re [0:N-1];
|
|
reg signed [DATA_W-1:0] out2_im [0:N-1];
|
|
|
|
// Input storage for roundtrip comparison
|
|
reg signed [DATA_W-1:0] in_re [0:N-1];
|
|
reg signed [DATA_W-1:0] in_im [0:N-1];
|
|
|
|
// ============================================================================
|
|
// HELPER TASKS
|
|
// ============================================================================
|
|
|
|
// Reset
|
|
task do_reset;
|
|
begin
|
|
reset_n = 0;
|
|
start = 0;
|
|
inverse = 0;
|
|
din_re = 0;
|
|
din_im = 0;
|
|
din_valid = 0;
|
|
repeat(5) @(posedge clk); #1;
|
|
reset_n = 1;
|
|
repeat(2) @(posedge clk); #1;
|
|
end
|
|
endtask
|
|
|
|
// Run FFT: load N samples from in_re/in_im arrays, capture output to out_re/out_im
|
|
task run_fft;
|
|
input inv;
|
|
integer i;
|
|
begin
|
|
inverse = inv;
|
|
@(posedge clk); #1;
|
|
start = 1;
|
|
@(posedge clk); #1;
|
|
start = 0;
|
|
|
|
// Feed N samples
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
din_re = in_re[i];
|
|
din_im = in_im[i];
|
|
din_valid = 1;
|
|
@(posedge clk); #1;
|
|
end
|
|
din_valid = 0;
|
|
din_re = 0;
|
|
din_im = 0;
|
|
|
|
// Wait for output and capture
|
|
out_idx = 0;
|
|
while (out_idx < N) begin
|
|
@(posedge clk); #1;
|
|
if (dout_valid) begin
|
|
out_re[out_idx] = dout_re;
|
|
out_im[out_idx] = dout_im;
|
|
out_idx = out_idx + 1;
|
|
end
|
|
end
|
|
|
|
// Wait for done
|
|
@(posedge clk); #1;
|
|
end
|
|
endtask
|
|
|
|
// Run FFT and capture to out2 arrays
|
|
task run_fft_to_out2;
|
|
input inv;
|
|
integer i;
|
|
begin
|
|
inverse = inv;
|
|
@(posedge clk); #1;
|
|
start = 1;
|
|
@(posedge clk); #1;
|
|
start = 0;
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
din_re = in_re[i];
|
|
din_im = in_im[i];
|
|
din_valid = 1;
|
|
@(posedge clk); #1;
|
|
end
|
|
din_valid = 0;
|
|
din_re = 0;
|
|
din_im = 0;
|
|
|
|
out_idx = 0;
|
|
while (out_idx < N) begin
|
|
@(posedge clk); #1;
|
|
if (dout_valid) begin
|
|
out2_re[out_idx] = dout_re;
|
|
out2_im[out_idx] = dout_im;
|
|
out_idx = out_idx + 1;
|
|
end
|
|
end
|
|
@(posedge clk); #1;
|
|
end
|
|
endtask
|
|
|
|
// ============================================================================
|
|
// VCD + CSV
|
|
// ============================================================================
|
|
initial begin
|
|
$dumpfile("tb_fft_engine.vcd");
|
|
$dumpvars(0, tb_fft_engine);
|
|
end
|
|
|
|
// ============================================================================
|
|
// MAIN TEST
|
|
// ============================================================================
|
|
integer i, j;
|
|
integer max_mag_bin;
|
|
reg signed [31:0] max_mag;
|
|
reg signed [31:0] mag;
|
|
reg signed [31:0] err;
|
|
integer max_err;
|
|
integer total_energy_in, total_energy_out;
|
|
|
|
// For tone generation
|
|
real angle;
|
|
reg signed [DATA_W-1:0] cos_val;
|
|
|
|
initial begin
|
|
pass_count = 0;
|
|
fail_count = 0;
|
|
|
|
$display("============================================================");
|
|
$display(" FFT Engine Testbench — N=%0d", N);
|
|
$display("============================================================");
|
|
|
|
do_reset;
|
|
|
|
// ================================================================
|
|
// TEST GROUP 1: Impulse Response
|
|
// FFT(delta[0]) should give all bins = 1 (in_re[0]=1, rest=0)
|
|
// Since input is Q15-ish (16-bit signed), use amplitude = 1000
|
|
// FFT of impulse with amplitude A: all bins = A
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 1: Impulse Response ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = (i == 0) ? 16'sd1000 : 16'sd0;
|
|
in_im[i] = 16'sd0;
|
|
end
|
|
|
|
run_fft(0); // Forward FFT
|
|
|
|
// All bins should have re ~= 1000, im ~= 0
|
|
max_err = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
err = out_re[i] - 1000;
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
err = out_im[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
end
|
|
$display(" Impulse FFT max error from expected: %0d", max_err);
|
|
check(max_err < 10, "Impulse FFT: all bins ~= input amplitude");
|
|
check(out_re[0] == 1000 || (out_re[0] >= 998 && out_re[0] <= 1002),
|
|
"Impulse FFT: bin 0 real ~= 1000");
|
|
|
|
// ================================================================
|
|
// TEST GROUP 2: DC Input
|
|
// FFT of constant value A across all N samples:
|
|
// bin 0 = A*N, all other bins = 0
|
|
// Use amplitude 100 so bin 0 = 100*32 = 3200
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 2: DC Input ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = 16'sd100;
|
|
in_im[i] = 16'sd0;
|
|
end
|
|
|
|
run_fft(0);
|
|
|
|
$display(" DC FFT bin[0] = %0d + j%0d (expect %0d + j0)", out_re[0], out_im[0], 100*N);
|
|
// Q15 twiddle rounding over N butterflies can cause ~1% error
|
|
check(out_re[0] >= (100*N - 50) && out_re[0] <= (100*N + 50),
|
|
"DC FFT: bin 0 real ~= A*N (1.5% tol)");
|
|
|
|
max_err = 0;
|
|
for (i = 1; i < N; i = i + 1) begin
|
|
mag = out_re[i] * out_re[i] + out_im[i] * out_im[i];
|
|
if (out_re[i] > max_err || -out_re[i] > max_err)
|
|
max_err = (out_re[i] > 0) ? out_re[i] : -out_re[i];
|
|
if (out_im[i] > max_err || -out_im[i] > max_err)
|
|
max_err = (out_im[i] > 0) ? out_im[i] : -out_im[i];
|
|
end
|
|
$display(" DC FFT max non-DC bin magnitude: %0d", max_err);
|
|
check(max_err < 20, "DC FFT: non-DC bins ~= 0 (Q15 rounding tol)");
|
|
|
|
// ================================================================
|
|
// TEST GROUP 3: Single Tone (cosine at bin 4)
|
|
// cos(2*pi*4*n/32) -> peaks at bins 4 and N-4=28
|
|
// Amplitude 1000 -> each peak = 1000*N/2 = 16000
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 3: Single Tone (bin 4) ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
// cos(2*pi*4*i/32) in Q15-ish
|
|
angle = 6.28318530718 * 4.0 * i / 32.0;
|
|
cos_val = $rtoi($cos(angle) * 1000.0);
|
|
in_re[i] = cos_val;
|
|
in_im[i] = 16'sd0;
|
|
end
|
|
|
|
run_fft(0);
|
|
|
|
// Find peak bin
|
|
max_mag = 0;
|
|
max_mag_bin = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
mag = out_re[i] * out_re[i] + out_im[i] * out_im[i];
|
|
if (mag > max_mag) begin
|
|
max_mag = mag;
|
|
max_mag_bin = i;
|
|
end
|
|
end
|
|
$display(" Tone FFT peak bin: %0d (expect 4)", max_mag_bin);
|
|
$display(" Tone FFT bin[4] = %0d + j%0d", out_re[4], out_im[4]);
|
|
$display(" Tone FFT bin[28] = %0d + j%0d", out_re[28], out_im[28]);
|
|
check(max_mag_bin == 4 || max_mag_bin == 28,
|
|
"Tone FFT: peak at bin 4 or 28");
|
|
// Bin 4 and 28 should have magnitude ~= N/2 * 1000 = 16000
|
|
mag = out_re[4] * out_re[4] + out_im[4] * out_im[4];
|
|
check(mag > 15000*15000 && mag < 17000*17000,
|
|
"Tone FFT: bin 4 magnitude ~= 16000");
|
|
|
|
// ================================================================
|
|
// TEST GROUP 4: Roundtrip (FFT then IFFT = identity)
|
|
// Load random-ish data, FFT, IFFT, compare to original
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 4: Roundtrip (FFT->IFFT) ---");
|
|
|
|
// Use a simple deterministic pattern
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = (i * 137 + 42) % 2001 - 1000; // [-1000, 1000]
|
|
in_im[i] = (i * 251 + 17) % 2001 - 1000;
|
|
end
|
|
|
|
// Forward FFT
|
|
run_fft(0);
|
|
|
|
// Copy FFT output as input for IFFT
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = out_re[i];
|
|
in_im[i] = out_im[i];
|
|
end
|
|
|
|
// Save original input for comparison
|
|
// (we need to recompute since in_re was overwritten)
|
|
|
|
// Actually let's redo: store originals first
|
|
// We'll do it properly with separate storage
|
|
|
|
// Re-do: load original pattern
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
out2_re[i] = (i * 137 + 42) % 2001 - 1000;
|
|
out2_im[i] = (i * 251 + 17) % 2001 - 1000;
|
|
end
|
|
|
|
// Now in_re/in_im has FFT output. Run IFFT.
|
|
run_fft(1);
|
|
|
|
// out_re/out_im should match original (out2_re/out2_im) within tolerance
|
|
max_err = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
err = out_re[i] - out2_re[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
err = out_im[i] - out2_im[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
end
|
|
$display(" Roundtrip max error: %0d", max_err);
|
|
check(max_err < 20, "Roundtrip: FFT->IFFT recovers original (err < 20)");
|
|
check(max_err < 5, "Roundtrip: FFT->IFFT tight tolerance (err < 5)");
|
|
|
|
// Print first few samples for debugging
|
|
$display(" Sample comparison (idx: original vs recovered):");
|
|
for (i = 0; i < 8; i = i + 1) begin
|
|
$display(" [%0d] re: %0d vs %0d, im: %0d vs %0d",
|
|
i, out2_re[i], out_re[i], out2_im[i], out_im[i]);
|
|
end
|
|
|
|
// ================================================================
|
|
// TEST GROUP 5: IFFT of impulse
|
|
// IFFT(delta[0]) = 1/N for all bins -> should be ~1 for amplitude N
|
|
// Input: bin[0] = N (=32), rest = 0
|
|
// IFFT output: all samples = 1
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 5: IFFT of Impulse ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = (i == 0) ? N : 16'sd0;
|
|
in_im[i] = 16'sd0;
|
|
end
|
|
|
|
run_fft(1); // Inverse FFT
|
|
|
|
max_err = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
err = out_re[i] - 1;
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
err = out_im[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
end
|
|
$display(" IFFT impulse max error: %0d", max_err);
|
|
check(max_err < 2, "IFFT impulse: all samples ~= 1");
|
|
|
|
// ================================================================
|
|
// TEST GROUP 6: Parseval's theorem (energy conservation)
|
|
// Sum |x[n]|^2 should equal (1/N) * Sum |X[k]|^2
|
|
// We compare N * sum_time vs sum_freq
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 6: Parseval's Theorem ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = (i * 137 + 42) % 2001 - 1000;
|
|
in_im[i] = (i * 251 + 17) % 2001 - 1000;
|
|
end
|
|
|
|
// Compute time-domain energy
|
|
total_energy_in = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
total_energy_in = total_energy_in + in_re[i] * in_re[i] + in_im[i] * in_im[i];
|
|
end
|
|
|
|
run_fft(0);
|
|
|
|
// Compute frequency-domain energy
|
|
total_energy_out = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
total_energy_out = total_energy_out + out_re[i] * out_re[i] + out_im[i] * out_im[i];
|
|
end
|
|
|
|
// Parseval: sum_time = (1/N) * sum_freq => N * sum_time = sum_freq
|
|
$display(" Time energy * N = %0d", total_energy_in * N);
|
|
$display(" Freq energy = %0d", total_energy_out);
|
|
// Allow some tolerance for fixed-point rounding
|
|
err = total_energy_in * N - total_energy_out;
|
|
if (err < 0) err = -err;
|
|
$display(" Parseval error = %0d", err);
|
|
// Relative error
|
|
if (total_energy_in * N > 0) begin
|
|
$display(" Parseval rel error = %0d%%", (err * 100) / (total_energy_in * N));
|
|
check((err * 100) / (total_energy_in * N) < 5,
|
|
"Parseval: energy conserved within 5%");
|
|
end
|
|
|
|
// ================================================================
|
|
// TEST GROUP 7: Pure imaginary input
|
|
// FFT of j*sin(2*pi*2*n/N) -> peaks at bins 2 and N-2
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 7: Pure Imaginary Tone (bin 2) ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = 16'sd0;
|
|
angle = 6.28318530718 * 2.0 * i / 32.0;
|
|
in_im[i] = $rtoi($sin(angle) * 1000.0);
|
|
end
|
|
|
|
run_fft(0);
|
|
|
|
// Find peak
|
|
max_mag = 0;
|
|
max_mag_bin = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
mag = out_re[i] * out_re[i] + out_im[i] * out_im[i];
|
|
if (mag > max_mag) begin
|
|
max_mag = mag;
|
|
max_mag_bin = i;
|
|
end
|
|
end
|
|
$display(" Imag tone peak bin: %0d (expect 2 or 30)", max_mag_bin);
|
|
check(max_mag_bin == 2 || max_mag_bin == 30,
|
|
"Imag tone: peak at bin 2 or 30");
|
|
|
|
// ================================================================
|
|
// TEST GROUP 8: Zero input
|
|
// ================================================================
|
|
$display("");
|
|
$display("--- Test Group 8: Zero Input ---");
|
|
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
in_re[i] = 16'sd0;
|
|
in_im[i] = 16'sd0;
|
|
end
|
|
|
|
run_fft(0);
|
|
|
|
max_err = 0;
|
|
for (i = 0; i < N; i = i + 1) begin
|
|
err = out_re[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
err = out_im[i];
|
|
if (err < 0) err = -err;
|
|
if (err > max_err) max_err = err;
|
|
end
|
|
check(max_err == 0, "Zero input: all output bins = 0");
|
|
|
|
// ================================================================
|
|
// SUMMARY
|
|
// ================================================================
|
|
$display("");
|
|
$display("============================================================");
|
|
$display(" RESULTS: %0d/%0d passed", pass_count, pass_count + fail_count);
|
|
if (fail_count == 0)
|
|
$display(" ALL TESTS PASSED");
|
|
else
|
|
$display(" SOME TESTS FAILED");
|
|
$display("============================================================");
|
|
|
|
$finish;
|
|
end
|
|
|
|
endmodule
|