Replace FFT stubs with synthesizable radix-2 DIT engine, fix BRAM inference

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.
This commit is contained in:
Jason
2026-03-16 10:25:07 +02:00
parent deb2e81ec4
commit 692b6a3bfa
9 changed files with 3428 additions and 190 deletions
+526
View File
@@ -0,0 +1,526 @@
`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
+543
View File
@@ -0,0 +1,543 @@
`timescale 1ns / 1ps
/**
* tb_mf_chain_synth.v
*
* Testbench for the SYNTHESIS branch of matched_filter_processing_chain.v.
* This is compiled WITHOUT -DSIMULATION so the `else` branch (fft_engine-based)
* is activated.
*
* The synthesis branch uses an iterative fft_engine (1024-pt, single butterfly),
* so processing takes ~40K+ clock cycles per frame. Timeouts are set accordingly.
*/
module tb_mf_chain_synth;
// Parameters
localparam CLK_PERIOD = 10.0; // 100 MHz
localparam FFT_SIZE = 1024;
// Timeout for full frame processing:
// 3 FFTs × ~12K cycles each + multiply ~1K + overhead 40K
// Use 200K for safety margin
localparam FRAME_TIMEOUT = 200000;
// Signals
reg clk;
reg reset_n;
reg [15:0] adc_data_i;
reg [15:0] adc_data_q;
reg adc_valid;
reg [5:0] chirp_counter;
reg [15:0] long_chirp_real;
reg [15:0] long_chirp_imag;
reg [15:0] short_chirp_real;
reg [15:0] short_chirp_imag;
wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q;
wire range_profile_valid;
wire [3:0] chain_state;
// Test bookkeeping
integer pass_count;
integer fail_count;
integer test_num;
integer i;
// Synthesis-branch states (mirror DUT)
localparam [3:0] ST_IDLE = 4'd0,
ST_COLLECT = 4'd1,
ST_SIG_FFT = 4'd2,
ST_SIG_CAP = 4'd3,
ST_REF_FFT = 4'd4,
ST_REF_CAP = 4'd5,
ST_MULTIPLY = 4'd6,
ST_INV_FFT = 4'd7,
ST_INV_CAP = 4'd8,
ST_OUTPUT = 4'd9,
ST_DONE = 4'd10;
// Concurrent output capture
integer cap_count;
reg cap_enable;
integer cap_max_abs;
integer cap_peak_bin;
integer cap_cur_abs;
// Output capture arrays
reg signed [15:0] cap_out_i [0:1023];
reg signed [15:0] cap_out_q [0:1023];
// Clock
always #(CLK_PERIOD/2) clk = ~clk;
// DUT
matched_filter_processing_chain uut (
.clk (clk),
.reset_n (reset_n),
.adc_data_i (adc_data_i),
.adc_data_q (adc_data_q),
.adc_valid (adc_valid),
.chirp_counter (chirp_counter),
.long_chirp_real (long_chirp_real),
.long_chirp_imag (long_chirp_imag),
.short_chirp_real (short_chirp_real),
.short_chirp_imag (short_chirp_imag),
.range_profile_i (range_profile_i),
.range_profile_q (range_profile_q),
.range_profile_valid (range_profile_valid),
.chain_state (chain_state)
);
// Concurrent output capture block
always @(posedge clk) begin
#1;
if (cap_enable && range_profile_valid) begin
if (cap_count < FFT_SIZE) begin
cap_out_i[cap_count] = range_profile_i;
cap_out_q[cap_count] = range_profile_q;
end
cap_cur_abs = (range_profile_i[15] ? -range_profile_i : range_profile_i)
+ (range_profile_q[15] ? -range_profile_q : range_profile_q);
if (cap_cur_abs > cap_max_abs) begin
cap_max_abs = cap_cur_abs;
cap_peak_bin = cap_count;
end
cap_count = cap_count + 1;
end
end
// Check task
task check;
input cond;
input [511:0] label;
begin
test_num = test_num + 1;
if (cond) begin
$display("[PASS] Test %0d: %0s", test_num, label);
pass_count = pass_count + 1;
end else begin
$display("[FAIL] Test %0d: %0s", test_num, label);
fail_count = fail_count + 1;
end
end
endtask
// Helper: apply reset
task apply_reset;
begin
reset_n = 0;
adc_valid = 0;
adc_data_i = 16'd0;
adc_data_q = 16'd0;
chirp_counter = 6'd0;
long_chirp_real = 16'd0;
long_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
cap_enable = 0;
cap_count = 0;
cap_max_abs = 0;
cap_peak_bin = -1;
repeat (4) @(posedge clk);
reset_n = 1;
@(posedge clk);
#1;
end
endtask
// Helper: start capture
task start_capture;
begin
cap_count = 0;
cap_max_abs = 0;
cap_peak_bin = -1;
cap_enable = 1;
end
endtask
// Helper: wait for IDLE with long timeout
task wait_for_idle;
integer wait_count;
begin
wait_count = 0;
while (chain_state != ST_IDLE && wait_count < FRAME_TIMEOUT) begin
@(posedge clk);
wait_count = wait_count + 1;
end
#1;
if (wait_count >= FRAME_TIMEOUT)
$display(" WARNING: wait_for_idle timed out at %0d cycles", wait_count);
end
endtask
// Helper: feed DC frame
task feed_dc_frame;
integer k;
begin
for (k = 0; k < FFT_SIZE; k = k + 1) begin
adc_data_i = 16'sh1000; // +4096
adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk);
#1;
end
adc_valid = 1'b0;
end
endtask
// Helper: feed tone frame (signal=reference=tone at bin)
task feed_tone_frame;
input integer tone_bin;
integer k;
real angle;
begin
for (k = 0; k < FFT_SIZE; k = k + 1) begin
angle = 6.28318530718 * tone_bin * k / (1.0 * FFT_SIZE);
adc_data_i = $rtoi(8000.0 * $cos(angle));
adc_data_q = $rtoi(8000.0 * $sin(angle));
long_chirp_real = $rtoi(8000.0 * $cos(angle));
long_chirp_imag = $rtoi(8000.0 * $sin(angle));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk);
#1;
end
adc_valid = 1'b0;
end
endtask
// Helper: feed impulse frame (delta at sample 0)
task feed_impulse_frame;
integer k;
begin
for (k = 0; k < FFT_SIZE; k = k + 1) begin
if (k == 0) begin
adc_data_i = 16'sh4000; // 0.5 in Q15
adc_data_q = 16'sh0000;
long_chirp_real = 16'sh4000;
long_chirp_imag = 16'sh0000;
end else begin
adc_data_i = 16'sh0000;
adc_data_q = 16'sh0000;
long_chirp_real = 16'sh0000;
long_chirp_imag = 16'sh0000;
end
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk);
#1;
end
adc_valid = 1'b0;
end
endtask
// Stimulus
initial begin
$dumpfile("tb_mf_chain_synth.vcd");
$dumpvars(0, tb_mf_chain_synth);
// Init
clk = 0;
pass_count = 0;
fail_count = 0;
test_num = 0;
cap_enable = 0;
cap_count = 0;
cap_max_abs = 0;
cap_peak_bin = -1;
//
// TEST GROUP 1: Reset behaviour
//
$display("\n--- Test Group 1: Reset Behaviour ---");
apply_reset;
reset_n = 0;
repeat (4) @(posedge clk); #1;
check(range_profile_valid === 1'b0, "range_profile_valid=0 during reset");
check(chain_state === ST_IDLE, "chain_state=IDLE during reset");
reset_n = 1;
@(posedge clk); #1;
//
// TEST GROUP 2: No valid input stays IDLE
//
$display("\n--- Test Group 2: No Valid Input Stays IDLE ---");
apply_reset;
repeat (100) @(posedge clk);
#1;
check(chain_state === ST_IDLE, "Stays in IDLE with no valid input");
check(range_profile_valid === 1'b0, "No output when no input");
//
// TEST GROUP 3: DC frame state transitions and output count
//
$display("\n--- Test Group 3: DC Frame Full Processing ---");
apply_reset;
start_capture;
feed_dc_frame;
$display(" Waiting for processing (3 FFTs + multiply)...");
wait_for_idle;
cap_enable = 0;
$display(" Output count: %0d (expected %0d)", cap_count, FFT_SIZE);
$display(" Peak bin: %0d, magnitude: %0d", cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "DC: Outputs exactly 1024 range profile samples");
check(chain_state === ST_IDLE, "DC: Returns to IDLE after frame");
// DC autocorrelation: FFT of DC = energy at bin 0 only
// conj multiply = |bin0|^2 at bin 0, zeros elsewhere
// IFFT of single bin = constant => peak at bin 0 (or any bin since all equal)
// With Q15 truncation, expect non-zero output
check(cap_max_abs > 0, "DC: Non-zero output");
//
// TEST GROUP 4: Zero input zero output
//
$display("\n--- Test Group 4: Zero Input Zero Output ---");
apply_reset;
start_capture;
for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'd0;
adc_data_q = 16'd0;
long_chirp_real = 16'd0;
long_chirp_imag = 16'd0;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk); #1;
end
adc_valid = 1'b0;
wait_for_idle;
cap_enable = 0;
$display(" Output count: %0d", cap_count);
$display(" Max magnitude: %0d", cap_max_abs);
check(cap_count == FFT_SIZE, "Zero: Got 1024 output samples");
// Allow small rounding noise (fft_engine Q15 rounding can produce ±1)
check(cap_max_abs <= 2, "Zero: Output magnitude <= 2 (near zero)");
//
// TEST GROUP 5: Tone autocorrelation (bin 5)
// signal = reference = tone at bin 5
// Autocorrelation peak at bin 0 (time lag 0)
//
$display("\n--- Test Group 5: Tone Autocorrelation (bin 5) ---");
apply_reset;
start_capture;
feed_tone_frame(5);
$display(" Waiting for processing...");
wait_for_idle;
cap_enable = 0;
$display(" Output count: %0d", cap_count);
$display(" Peak bin: %0d, magnitude: %0d", cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "Tone: Got 1024 output samples");
// Autocorrelation of a pure tone: peak at bin 0
check(cap_peak_bin <= 5 || cap_peak_bin >= FFT_SIZE - 5,
"Tone: Autocorrelation peak near bin 0");
check(cap_max_abs > 0, "Tone: Peak magnitude > 0");
//
// TEST GROUP 6: Impulse autocorrelation
//
$display("\n--- Test Group 6: Impulse Autocorrelation ---");
apply_reset;
start_capture;
feed_impulse_frame;
$display(" Waiting for processing...");
wait_for_idle;
cap_enable = 0;
$display(" Output count: %0d", cap_count);
$display(" Peak bin: %0d, magnitude: %0d", cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "Impulse: Got 1024 output samples");
check(cap_max_abs > 0, "Impulse: Non-zero output");
check(chain_state === ST_IDLE, "Impulse: Returns to IDLE");
//
// TEST GROUP 7: Reset mid-operation
//
$display("\n--- Test Group 7: Reset Mid-Operation ---");
apply_reset;
// Feed ~512 samples (halfway through collection)
for (i = 0; i < 512; i = i + 1) begin
adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk); #1;
end
adc_valid = 1'b0;
// Assert reset
reset_n = 0;
repeat (4) @(posedge clk); #1;
reset_n = 1;
@(posedge clk); #1;
check(chain_state === ST_IDLE, "Mid-op reset: Returns to IDLE");
check(range_profile_valid === 1'b0, "Mid-op reset: No output");
// Feed a complete frame after reset
start_capture;
feed_dc_frame;
wait_for_idle;
cap_enable = 0;
$display(" Post-reset frame: %0d outputs", cap_count);
check(cap_count == FFT_SIZE, "Mid-op reset: Post-reset frame gives 1024 outputs");
//
// TEST GROUP 8: Back-to-back frames
//
$display("\n--- Test Group 8: Back-to-Back Frames ---");
apply_reset;
// Frame 1
start_capture;
feed_dc_frame;
wait_for_idle;
cap_enable = 0;
$display(" Frame 1: %0d outputs, peak=%0d, mag=%0d", cap_count, cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "B2B Frame 1: 1024 outputs");
// Frame 2
start_capture;
feed_tone_frame(3);
wait_for_idle;
cap_enable = 0;
$display(" Frame 2: %0d outputs, peak=%0d, mag=%0d", cap_count, cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "B2B Frame 2: 1024 outputs");
//
// TEST GROUP 9: Mismatched signal vs reference
// Signal at bin 5, reference at bin 10
//
$display("\n--- Test Group 9: Mismatched Signal vs Reference ---");
apply_reset;
start_capture;
for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = $rtoi(8000.0 * $cos(6.28318530718 * 5 * i / 1024.0));
adc_data_q = $rtoi(8000.0 * $sin(6.28318530718 * 5 * i / 1024.0));
long_chirp_real = $rtoi(8000.0 * $cos(6.28318530718 * 10 * i / 1024.0));
long_chirp_imag = $rtoi(8000.0 * $sin(6.28318530718 * 10 * i / 1024.0));
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk); #1;
end
adc_valid = 1'b0;
wait_for_idle;
cap_enable = 0;
$display(" Mismatched: peak bin=%0d, magnitude=%0d", cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "Mismatch: Got 1024 output samples");
// Signal=bin5, ref=bin10: product has energy at bin(5-10)=bin(-5)=bin(1019)
// IFFT of that gives a tone at sample spacing of 5
// The key check is that it completes and produces output
check(cap_max_abs > 0, "Mismatch: Non-zero output");
check(chain_state === ST_IDLE, "Mismatch: Returns to IDLE");
//
// TEST GROUP 10: Saturation max positive values
//
$display("\n--- Test Group 10: Saturation Max Positive ---");
apply_reset;
start_capture;
for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh7FFF;
adc_data_q = 16'sh7FFF;
long_chirp_real = 16'sh7FFF;
long_chirp_imag = 16'sh7FFF;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk); #1;
end
adc_valid = 1'b0;
wait_for_idle;
cap_enable = 0;
$display(" Saturation: count=%0d, peak=%0d, mag=%0d", cap_count, cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "Saturation: Completes with 1024 outputs");
check(chain_state === ST_IDLE, "Saturation: Returns to IDLE");
//
// TEST GROUP 11: Valid-gap / stall test
//
$display("\n--- Test Group 11: Valid-Gap Stall Test ---");
apply_reset;
start_capture;
for (i = 0; i < FFT_SIZE; i = i + 1) begin
adc_data_i = 16'sh1000;
adc_data_q = 16'sh0000;
long_chirp_real = 16'sh1000;
long_chirp_imag = 16'sh0000;
short_chirp_real = 16'd0;
short_chirp_imag = 16'd0;
adc_valid = 1'b1;
@(posedge clk); #1;
// Every 100 samples, insert a 10-cycle gap
if ((i % 100) == 99 && i < FFT_SIZE - 1) begin : stall_block
integer gap_j;
adc_valid = 1'b0;
for (gap_j = 0; gap_j < 10; gap_j = gap_j + 1) begin
@(posedge clk); #1;
end
end
end
adc_valid = 1'b0;
wait_for_idle;
cap_enable = 0;
$display(" Stall: count=%0d, peak=%0d, mag=%0d", cap_count, cap_peak_bin, cap_max_abs);
check(cap_count == FFT_SIZE, "Stall: 1024 outputs emitted");
check(chain_state === ST_IDLE, "Stall: Returns to IDLE");
//
// Summary
//
$display("");
$display("========================================");
$display(" MATCHED FILTER PROCESSING CHAIN");
$display(" (SYNTHESIS BRANCH fft_engine)");
$display(" PASSED: %0d / %0d", pass_count, test_num);
$display(" FAILED: %0d / %0d", fail_count, test_num);
if (fail_count == 0)
$display(" ** ALL TESTS PASSED **");
else
$display(" ** SOME TESTS FAILED **");
$display("========================================");
$display("");
#100;
$finish;
end
endmodule
+355
View File
@@ -0,0 +1,355 @@
`timescale 1ns / 1ps
/**
* tb_xfft_32.v
*
* Testbench for xfft_32 AXI-Stream FFT wrapper.
* Verifies the wrapper correctly interfaces with fft_engine via AXI-Stream.
*
* Test Groups:
* 1. Impulse response (all output bins = input amplitude)
* 2. DC input (bin 0 = A*N, rest ~= 0)
* 3. Single tone detection
* 4. AXI-Stream handshake correctness (tvalid, tlast, tready)
* 5. Back-to-back transforms (no state leakage)
*/
module tb_xfft_32;
// ============================================================================
// PARAMETERS
// ============================================================================
localparam N = 32;
localparam CLK_PERIOD = 10;
// ============================================================================
// SIGNALS
// ============================================================================
reg aclk, aresetn;
reg [7:0] cfg_tdata;
reg cfg_tvalid;
wire cfg_tready;
reg [31:0] din_tdata;
reg din_tvalid;
reg din_tlast;
wire [31:0] dout_tdata;
wire dout_tvalid;
wire dout_tlast;
reg dout_tready;
// ============================================================================
// DUT
// ============================================================================
xfft_32 dut (
.aclk(aclk),
.aresetn(aresetn),
.s_axis_config_tdata(cfg_tdata),
.s_axis_config_tvalid(cfg_tvalid),
.s_axis_config_tready(cfg_tready),
.s_axis_data_tdata(din_tdata),
.s_axis_data_tvalid(din_tvalid),
.s_axis_data_tlast(din_tlast),
.m_axis_data_tdata(dout_tdata),
.m_axis_data_tvalid(dout_tvalid),
.m_axis_data_tlast(dout_tlast),
.m_axis_data_tready(dout_tready)
);
// ============================================================================
// CLOCK
// ============================================================================
initial aclk = 0;
always #(CLK_PERIOD/2) aclk = ~aclk;
// ============================================================================
// 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
// ============================================================================
// OUTPUT CAPTURE
// ============================================================================
reg signed [15:0] out_re [0:N-1];
reg signed [15:0] out_im [0:N-1];
integer out_idx;
reg got_tlast;
integer tlast_count;
// ============================================================================
// HELPER TASKS
// ============================================================================
task do_reset;
begin
aresetn = 0;
cfg_tdata = 0;
cfg_tvalid = 0;
din_tdata = 0;
din_tvalid = 0;
din_tlast = 0;
dout_tready = 1;
repeat(5) @(posedge aclk);
aresetn = 1;
repeat(2) @(posedge aclk);
end
endtask
// Send config (forward FFT: tdata[0]=1)
// Waits for cfg_tready (wrapper in S_IDLE) before sending
task send_config;
input [7:0] cfg;
integer wait_cnt;
begin
// Wait for wrapper to be ready (S_IDLE)
wait_cnt = 0;
while (!cfg_tready && wait_cnt < 5000) begin
@(posedge aclk);
wait_cnt = wait_cnt + 1;
end
cfg_tdata = cfg;
cfg_tvalid = 1;
@(posedge aclk);
cfg_tvalid = 0;
cfg_tdata = 0;
end
endtask
// Feed N samples: each sample is {im[15:0], re[15:0]}
// in_re_arr and in_im_arr must be pre-loaded
reg signed [15:0] feed_re [0:N-1];
reg signed [15:0] feed_im [0:N-1];
task feed_data;
integer i;
begin
for (i = 0; i < N; i = i + 1) begin
din_tdata = {feed_im[i], feed_re[i]};
din_tvalid = 1;
din_tlast = (i == N - 1) ? 1 : 0;
@(posedge aclk);
end
din_tvalid = 0;
din_tlast = 0;
din_tdata = 0;
end
endtask
// Capture N output samples
task capture_output;
integer timeout;
begin
out_idx = 0;
got_tlast = 0;
tlast_count = 0;
timeout = 0;
while (out_idx < N && timeout < 5000) begin
@(posedge aclk);
if (dout_tvalid && dout_tready) begin
out_re[out_idx] = dout_tdata[15:0];
out_im[out_idx] = dout_tdata[31:16];
if (dout_tlast) begin
got_tlast = 1;
tlast_count = tlast_count + 1;
end
out_idx = out_idx + 1;
end
timeout = timeout + 1;
end
end
endtask
// ============================================================================
// VCD
// ============================================================================
initial begin
$dumpfile("tb_xfft_32.vcd");
$dumpvars(0, tb_xfft_32);
end
// ============================================================================
// MAIN TEST
// ============================================================================
integer i;
reg signed [31:0] err;
integer max_err;
integer max_mag_bin;
reg signed [31:0] max_mag, mag;
real angle;
initial begin
pass_count = 0;
fail_count = 0;
$display("============================================================");
$display(" xfft_32 AXI-Stream Wrapper Testbench");
$display("============================================================");
do_reset;
// ================================================================
// TEST 1: Impulse Response
// ================================================================
$display("");
$display("--- Test 1: Impulse Response ---");
for (i = 0; i < N; i = i + 1) begin
feed_re[i] = (i == 0) ? 16'sd1000 : 16'sd0;
feed_im[i] = 16'sd0;
end
send_config(8'h01); // Forward FFT
feed_data;
capture_output;
check(out_idx == N, "Received N output samples");
check(got_tlast == 1, "Got tlast on output");
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 max error: %0d", max_err);
check(max_err < 10, "Impulse: all bins ~= 1000");
// ================================================================
// TEST 2: DC Input
// ================================================================
$display("");
$display("--- Test 2: DC Input ---");
for (i = 0; i < N; i = i + 1) begin
feed_re[i] = 16'sd100;
feed_im[i] = 16'sd0;
end
send_config(8'h01);
feed_data;
capture_output;
$display(" DC bin[0] = %0d + j%0d (expect ~3200)", out_re[0], out_im[0]);
check(out_re[0] >= 3100 && out_re[0] <= 3300, "DC: bin 0 ~= 3200 (5% tol)");
max_err = 0;
for (i = 1; 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
$display(" DC max non-DC: %0d", max_err);
check(max_err < 25, "DC: non-DC bins ~= 0");
// ================================================================
// TEST 3: Single Tone (bin 4)
// ================================================================
$display("");
$display("--- Test 3: Single Tone (bin 4) ---");
for (i = 0; i < N; i = i + 1) begin
angle = 6.28318530718 * 4.0 * i / 32.0;
feed_re[i] = $rtoi($cos(angle) * 1000.0);
feed_im[i] = 16'sd0;
end
send_config(8'h01);
feed_data;
capture_output;
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 peak bin: %0d (expect 4 or 28)", max_mag_bin);
check(max_mag_bin == 4 || max_mag_bin == 28, "Tone: peak at bin 4 or 28");
// ================================================================
// TEST 4: Back-to-back transforms
// ================================================================
$display("");
$display("--- Test 4: Back-to-Back Transforms ---");
// First: impulse
for (i = 0; i < N; i = i + 1) begin
feed_re[i] = (i == 0) ? 16'sd500 : 16'sd0;
feed_im[i] = 16'sd0;
end
send_config(8'h01);
feed_data;
capture_output;
check(out_idx == N, "Back-to-back 1st: got N outputs");
// Second: DC immediately after
for (i = 0; i < N; i = i + 1) begin
feed_re[i] = 16'sd50;
feed_im[i] = 16'sd0;
end
send_config(8'h01);
feed_data;
capture_output;
check(out_idx == N, "Back-to-back 2nd: got N outputs");
$display(" 2nd transform bin[0] = %0d (expect ~1600)", out_re[0]);
check(out_re[0] >= 1500 && out_re[0] <= 1700, "Back-to-back 2nd: bin 0 ~= 1600");
// ================================================================
// TEST 5: Zero input
// ================================================================
$display("");
$display("--- Test 5: Zero Input ---");
for (i = 0; i < N; i = i + 1) begin
feed_re[i] = 16'sd0;
feed_im[i] = 16'sd0;
end
send_config(8'h01);
feed_data;
capture_output;
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 outputs = 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