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:
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user