Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7742b517b6 | |||
| fa5e1dcdf4 | |||
| ade1497457 | |||
| f1d3bff4fe | |||
| 791b2e7374 | |||
| 15a9cde274 | |||
| ae7643975d | |||
| 029df375f5 | |||
| a9ceb3c851 | |||
| 425c349184 |
@@ -137,6 +137,145 @@ module cdc_adc_to_processing #(
|
|||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ASYNC FIFO FOR CONTINUOUS SAMPLE STREAMS
|
||||||
|
// ============================================================================
|
||||||
|
// Replaces cdc_adc_to_processing for the DDC path where the CIC decimator
|
||||||
|
// produces samples at ~100 MSPS from a 400 MHz clock and the consumer runs
|
||||||
|
// at 100 MHz. Gray-coded read/write pointers (the only valid use of Gray
|
||||||
|
// encoding across clock domains) ensure no data corruption or loss.
|
||||||
|
//
|
||||||
|
// Depth must be a power of 2. Default 8 entries gives comfortable margin
|
||||||
|
// for the 4:1 decimated stream (1 sample per 4 src clocks, 1 consumer
|
||||||
|
// clock per sample).
|
||||||
|
// ============================================================================
|
||||||
|
module cdc_async_fifo #(
|
||||||
|
parameter WIDTH = 18,
|
||||||
|
parameter DEPTH = 8, // Must be power of 2
|
||||||
|
parameter ADDR_BITS = 3 // log2(DEPTH)
|
||||||
|
)(
|
||||||
|
// Write (source) domain
|
||||||
|
input wire wr_clk,
|
||||||
|
input wire wr_reset_n,
|
||||||
|
input wire [WIDTH-1:0] wr_data,
|
||||||
|
input wire wr_en,
|
||||||
|
output wire wr_full,
|
||||||
|
|
||||||
|
// Read (destination) domain
|
||||||
|
input wire rd_clk,
|
||||||
|
input wire rd_reset_n,
|
||||||
|
output wire [WIDTH-1:0] rd_data,
|
||||||
|
output wire rd_valid,
|
||||||
|
input wire rd_ack // Consumer asserts to pop
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gray code conversion functions
|
||||||
|
function [ADDR_BITS:0] bin2gray;
|
||||||
|
input [ADDR_BITS:0] bin;
|
||||||
|
bin2gray = bin ^ (bin >> 1);
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
function [ADDR_BITS:0] gray2bin;
|
||||||
|
input [ADDR_BITS:0] gray;
|
||||||
|
reg [ADDR_BITS:0] bin;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
bin[ADDR_BITS] = gray[ADDR_BITS];
|
||||||
|
for (k = ADDR_BITS-1; k >= 0; k = k - 1)
|
||||||
|
bin[k] = bin[k+1] ^ gray[k];
|
||||||
|
gray2bin = bin;
|
||||||
|
end
|
||||||
|
endfunction
|
||||||
|
|
||||||
|
// ------- Pointer declarations (both domains, before use) -------
|
||||||
|
// Write domain pointers
|
||||||
|
reg [ADDR_BITS:0] wr_ptr_bin = 0; // Extra bit for full/empty
|
||||||
|
reg [ADDR_BITS:0] wr_ptr_gray = 0;
|
||||||
|
|
||||||
|
// Read domain pointers (declared here so write domain can synchronize them)
|
||||||
|
reg [ADDR_BITS:0] rd_ptr_bin = 0;
|
||||||
|
reg [ADDR_BITS:0] rd_ptr_gray = 0;
|
||||||
|
|
||||||
|
// ------- Write domain -------
|
||||||
|
|
||||||
|
// Synchronized read pointer in write domain (scalar regs, not memory
|
||||||
|
// arrays — avoids iverilog sensitivity/NBA bugs on array elements and
|
||||||
|
// gives synthesis explicit flop names for ASYNC_REG constraints)
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [ADDR_BITS:0] rd_ptr_gray_sync0 = 0;
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [ADDR_BITS:0] rd_ptr_gray_sync1 = 0;
|
||||||
|
|
||||||
|
// FIFO memory (inferred as distributed RAM — small depth)
|
||||||
|
reg [WIDTH-1:0] mem [0:DEPTH-1];
|
||||||
|
|
||||||
|
wire wr_addr_match = (wr_ptr_gray == rd_ptr_gray_sync1);
|
||||||
|
wire wr_wrap_match = (wr_ptr_gray[ADDR_BITS] != rd_ptr_gray_sync1[ADDR_BITS]) &&
|
||||||
|
(wr_ptr_gray[ADDR_BITS-1] != rd_ptr_gray_sync1[ADDR_BITS-1]) &&
|
||||||
|
(wr_ptr_gray[ADDR_BITS-2:0] == rd_ptr_gray_sync1[ADDR_BITS-2:0]);
|
||||||
|
assign wr_full = wr_wrap_match;
|
||||||
|
|
||||||
|
always @(posedge wr_clk) begin
|
||||||
|
if (!wr_reset_n) begin
|
||||||
|
wr_ptr_bin <= 0;
|
||||||
|
wr_ptr_gray <= 0;
|
||||||
|
rd_ptr_gray_sync0 <= 0;
|
||||||
|
rd_ptr_gray_sync1 <= 0;
|
||||||
|
end else begin
|
||||||
|
// Synchronize read pointer into write domain
|
||||||
|
rd_ptr_gray_sync0 <= rd_ptr_gray;
|
||||||
|
rd_ptr_gray_sync1 <= rd_ptr_gray_sync0;
|
||||||
|
|
||||||
|
// Write
|
||||||
|
if (wr_en && !wr_full) begin
|
||||||
|
mem[wr_ptr_bin[ADDR_BITS-1:0]] <= wr_data;
|
||||||
|
wr_ptr_bin <= wr_ptr_bin + 1;
|
||||||
|
wr_ptr_gray <= bin2gray(wr_ptr_bin + 1);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// ------- Read domain -------
|
||||||
|
|
||||||
|
// Synchronized write pointer in read domain (scalar regs — see above)
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [ADDR_BITS:0] wr_ptr_gray_sync0 = 0;
|
||||||
|
(* ASYNC_REG = "TRUE" *) reg [ADDR_BITS:0] wr_ptr_gray_sync1 = 0;
|
||||||
|
|
||||||
|
wire rd_empty = (rd_ptr_gray == wr_ptr_gray_sync1);
|
||||||
|
|
||||||
|
// Output register — holds data until consumed
|
||||||
|
reg [WIDTH-1:0] rd_data_reg = 0;
|
||||||
|
reg rd_valid_reg = 0;
|
||||||
|
|
||||||
|
always @(posedge rd_clk) begin
|
||||||
|
if (!rd_reset_n) begin
|
||||||
|
rd_ptr_bin <= 0;
|
||||||
|
rd_ptr_gray <= 0;
|
||||||
|
wr_ptr_gray_sync0 <= 0;
|
||||||
|
wr_ptr_gray_sync1 <= 0;
|
||||||
|
rd_data_reg <= 0;
|
||||||
|
rd_valid_reg <= 0;
|
||||||
|
end else begin
|
||||||
|
// Synchronize write pointer into read domain
|
||||||
|
wr_ptr_gray_sync0 <= wr_ptr_gray;
|
||||||
|
wr_ptr_gray_sync1 <= wr_ptr_gray_sync0;
|
||||||
|
|
||||||
|
// Pop logic: present data when FIFO not empty
|
||||||
|
if (!rd_empty && (!rd_valid_reg || rd_ack)) begin
|
||||||
|
rd_data_reg <= mem[rd_ptr_bin[ADDR_BITS-1:0]];
|
||||||
|
rd_valid_reg <= 1'b1;
|
||||||
|
rd_ptr_bin <= rd_ptr_bin + 1;
|
||||||
|
rd_ptr_gray <= bin2gray(rd_ptr_bin + 1);
|
||||||
|
end else if (rd_valid_reg && rd_ack) begin
|
||||||
|
// Consumer took data but FIFO is empty now
|
||||||
|
rd_valid_reg <= 1'b0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
assign rd_data = rd_data_reg;
|
||||||
|
assign rd_valid = rd_valid_reg;
|
||||||
|
|
||||||
|
endmodule
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// CDC FOR SINGLE BIT SIGNALS
|
// CDC FOR SINGLE BIT SIGNALS
|
||||||
// Uses synchronous reset on sync chain to avoid metastability on reset
|
// Uses synchronous reset on sync chain to avoid metastability on reset
|
||||||
|
|||||||
@@ -584,41 +584,59 @@ cic_decimator_4x_enhanced cic_q_inst (
|
|||||||
assign cic_valid = cic_valid_i & cic_valid_q;
|
assign cic_valid = cic_valid_i & cic_valid_q;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Enhanced FIR Filters with FIXED valid signal handling
|
// Clock Domain Crossing: 400 MHz CIC output → 100 MHz FIR input
|
||||||
// NOTE: Wire declarations moved BEFORE CDC instances to fix forward-reference
|
// ============================================================================
|
||||||
// error in Icarus Verilog (was originally after CDC instantiation)
|
// The CIC decimates 4:1, producing one sample per 4 clk_400m cycles (~100 MSPS).
|
||||||
|
// The FIR runs at clk_100m (100 MHz). The two clocks have unknown phase
|
||||||
|
// relationship, so a proper asynchronous FIFO with Gray-coded pointers is
|
||||||
|
// required. The old cdc_adc_to_processing module Gray-encoded the sample
|
||||||
|
// DATA which is invalid (Gray encoding only guarantees single-bit transitions
|
||||||
|
// for monotonically incrementing counters, not arbitrary sample values).
|
||||||
|
//
|
||||||
|
// Depth 8 provides margin: worst case, 2 samples can be in flight before
|
||||||
|
// the read side pops, well within a depth-8 budget.
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
wire fir_in_valid_i, fir_in_valid_q;
|
wire fir_in_valid_i, fir_in_valid_q;
|
||||||
wire fir_valid_i, fir_valid_q;
|
wire fir_valid_i, fir_valid_q;
|
||||||
wire fir_i_ready, fir_q_ready;
|
wire fir_i_ready, fir_q_ready;
|
||||||
wire [17:0] fir_d_in_i, fir_d_in_q;
|
wire [17:0] fir_d_in_i, fir_d_in_q;
|
||||||
|
|
||||||
cdc_adc_to_processing #(
|
// I-channel CDC: async FIFO, 400 MHz write → 100 MHz read
|
||||||
.WIDTH(18),
|
cdc_async_fifo #(
|
||||||
.STAGES(3)
|
.WIDTH(18),
|
||||||
)CDC_FIR_i(
|
.DEPTH(8),
|
||||||
.src_clk(clk_400m),
|
.ADDR_BITS(3)
|
||||||
.dst_clk(clk_100m),
|
) CDC_FIR_i (
|
||||||
.src_reset_n(reset_n_400m),
|
.wr_clk(clk_400m),
|
||||||
.dst_reset_n(reset_n),
|
.wr_reset_n(reset_n_400m),
|
||||||
.src_data(cic_i_out),
|
.wr_data(cic_i_out),
|
||||||
.src_valid(cic_valid_i),
|
.wr_en(cic_valid_i),
|
||||||
.dst_data(fir_d_in_i),
|
.wr_full(), // At 1:1 data rate, overflow should not occur
|
||||||
.dst_valid(fir_in_valid_i)
|
|
||||||
|
.rd_clk(clk_100m),
|
||||||
|
.rd_reset_n(reset_n),
|
||||||
|
.rd_data(fir_d_in_i),
|
||||||
|
.rd_valid(fir_in_valid_i),
|
||||||
|
.rd_ack(fir_in_valid_i) // Auto-pop: consume every valid sample
|
||||||
);
|
);
|
||||||
|
|
||||||
cdc_adc_to_processing #(
|
// Q-channel CDC: async FIFO, 400 MHz write → 100 MHz read
|
||||||
.WIDTH(18),
|
cdc_async_fifo #(
|
||||||
.STAGES(3)
|
.WIDTH(18),
|
||||||
)CDC_FIR_q(
|
.DEPTH(8),
|
||||||
.src_clk(clk_400m),
|
.ADDR_BITS(3)
|
||||||
.dst_clk(clk_100m),
|
) CDC_FIR_q (
|
||||||
.src_reset_n(reset_n_400m),
|
.wr_clk(clk_400m),
|
||||||
.dst_reset_n(reset_n),
|
.wr_reset_n(reset_n_400m),
|
||||||
.src_data(cic_q_out),
|
.wr_data(cic_q_out),
|
||||||
.src_valid(cic_valid_q),
|
.wr_en(cic_valid_q),
|
||||||
.dst_data(fir_d_in_q),
|
.wr_full(),
|
||||||
.dst_valid(fir_in_valid_q)
|
|
||||||
|
.rd_clk(clk_100m),
|
||||||
|
.rd_reset_n(reset_n),
|
||||||
|
.rd_data(fir_d_in_q),
|
||||||
|
.rd_valid(fir_in_valid_q),
|
||||||
|
.rd_ack(fir_in_valid_q)
|
||||||
);
|
);
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -531,6 +531,23 @@ xfft_16 fft_inst (
|
|||||||
// Status Outputs
|
// Status Outputs
|
||||||
// ==============================================
|
// ==============================================
|
||||||
assign processing_active = (state != S_IDLE);
|
assign processing_active = (state != S_IDLE);
|
||||||
assign frame_complete = (state == S_IDLE && frame_buffer_full == 0);
|
|
||||||
|
// frame_complete must be a single-cycle pulse, not a level.
|
||||||
|
// The AGC (rx_gain_control) uses this as frame_boundary to snapshot
|
||||||
|
// per-frame metrics and update gain. If held high continuously,
|
||||||
|
// the AGC would re-evaluate every clock with zeroed accumulators,
|
||||||
|
// collapsing saturation_count/peak_magnitude to zero.
|
||||||
|
//
|
||||||
|
// Detect the falling edge of processing_active: the exact clock
|
||||||
|
// when the Doppler processor finishes all sub-frame FFTs and
|
||||||
|
// returns to S_IDLE with the frame buffer drained.
|
||||||
|
reg processing_active_prev;
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n)
|
||||||
|
processing_active_prev <= 1'b0;
|
||||||
|
else
|
||||||
|
processing_active_prev <= processing_active;
|
||||||
|
end
|
||||||
|
assign frame_complete = (~processing_active & processing_active_prev);
|
||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ reg signed [15:0] buf_rdata_i, buf_rdata_q;
|
|||||||
// State machine
|
// State machine
|
||||||
reg [3:0] state;
|
reg [3:0] state;
|
||||||
localparam ST_IDLE = 0;
|
localparam ST_IDLE = 0;
|
||||||
|
localparam ST_WAIT_LISTEN = 9; // Wait for TX chirp to end before collecting
|
||||||
localparam ST_COLLECT_DATA = 1;
|
localparam ST_COLLECT_DATA = 1;
|
||||||
localparam ST_ZERO_PAD = 2;
|
localparam ST_ZERO_PAD = 2;
|
||||||
localparam ST_WAIT_REF = 3;
|
localparam ST_WAIT_REF = 3;
|
||||||
@@ -98,11 +99,22 @@ reg signed [15:0] overlap_cache_i [0:OVERLAP_SAMPLES-1];
|
|||||||
reg signed [15:0] overlap_cache_q [0:OVERLAP_SAMPLES-1];
|
reg signed [15:0] overlap_cache_q [0:OVERLAP_SAMPLES-1];
|
||||||
reg [7:0] overlap_copy_count;
|
reg [7:0] overlap_copy_count;
|
||||||
|
|
||||||
|
// Listen-window delay counter: skip TX chirp duration before collecting echoes.
|
||||||
|
// The chirp_start_pulse fires at the beginning of TX, but the matched filter
|
||||||
|
// must collect receive-window samples (echoes), not TX leakage.
|
||||||
|
// For long chirp: skip LONG_CHIRP_SAMPLES (3000) ddc_valid counts
|
||||||
|
// For short chirp: skip SHORT_CHIRP_SAMPLES (50) ddc_valid counts
|
||||||
|
reg [15:0] listen_delay_count;
|
||||||
|
reg [15:0] listen_delay_target;
|
||||||
|
|
||||||
// Microcontroller sync detection
|
// Microcontroller sync detection
|
||||||
|
// mc_new_chirp/elevation/azimuth are TOGGLE signals from radar_mode_controller:
|
||||||
|
// they invert on every event. Detect ANY transition (XOR with previous value),
|
||||||
|
// not just rising edge, otherwise every other chirp/elevation/azimuth is missed.
|
||||||
reg mc_new_chirp_prev, mc_new_elevation_prev, mc_new_azimuth_prev;
|
reg mc_new_chirp_prev, mc_new_elevation_prev, mc_new_azimuth_prev;
|
||||||
wire chirp_start_pulse = mc_new_chirp && !mc_new_chirp_prev;
|
wire chirp_start_pulse = mc_new_chirp ^ mc_new_chirp_prev;
|
||||||
wire elevation_change_pulse = mc_new_elevation && !mc_new_elevation_prev;
|
wire elevation_change_pulse = mc_new_elevation ^ mc_new_elevation_prev;
|
||||||
wire azimuth_change_pulse = mc_new_azimuth && !mc_new_azimuth_prev;
|
wire azimuth_change_pulse = mc_new_azimuth ^ mc_new_azimuth_prev;
|
||||||
|
|
||||||
// Processing chain signals
|
// Processing chain signals
|
||||||
wire [15:0] fft_pc_i, fft_pc_q;
|
wire [15:0] fft_pc_i, fft_pc_q;
|
||||||
@@ -184,6 +196,8 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
buf_wdata_q <= 0;
|
buf_wdata_q <= 0;
|
||||||
buf_raddr <= 0;
|
buf_raddr <= 0;
|
||||||
overlap_copy_count <= 0;
|
overlap_copy_count <= 0;
|
||||||
|
listen_delay_count <= 0;
|
||||||
|
listen_delay_target <= 0;
|
||||||
end else begin
|
end else begin
|
||||||
pc_valid <= 0;
|
pc_valid <= 0;
|
||||||
mem_request <= 0;
|
mem_request <= 0;
|
||||||
@@ -205,19 +219,45 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
|
|
||||||
// Wait for chirp start from microcontroller
|
// Wait for chirp start from microcontroller
|
||||||
if (chirp_start_pulse) begin
|
if (chirp_start_pulse) begin
|
||||||
state <= ST_COLLECT_DATA;
|
|
||||||
total_segments <= use_long_chirp ? LONG_SEGMENTS[2:0] : SHORT_SEGMENTS[2:0];
|
total_segments <= use_long_chirp ? LONG_SEGMENTS[2:0] : SHORT_SEGMENTS[2:0];
|
||||||
|
|
||||||
|
// Delay collection until the listen window opens.
|
||||||
|
// chirp_start_pulse fires at TX start; echoes only arrive
|
||||||
|
// after the chirp finishes. Skip the TX duration by
|
||||||
|
// counting ddc_valid pulses before entering ST_COLLECT_DATA.
|
||||||
|
listen_delay_count <= 0;
|
||||||
|
listen_delay_target <= use_long_chirp ? LONG_CHIRP_SAMPLES[15:0]
|
||||||
|
: SHORT_CHIRP_SAMPLES[15:0];
|
||||||
|
state <= ST_WAIT_LISTEN;
|
||||||
|
|
||||||
`ifdef SIMULATION
|
`ifdef SIMULATION
|
||||||
$display("[MULTI_SEG_FIXED] Starting %s chirp, segments: %d",
|
$display("[MULTI_SEG_FIXED] Chirp start detected, waiting for listen window (%0d samples)",
|
||||||
use_long_chirp ? "LONG" : "SHORT",
|
use_long_chirp ? LONG_CHIRP_SAMPLES : SHORT_CHIRP_SAMPLES);
|
||||||
use_long_chirp ? LONG_SEGMENTS : SHORT_SEGMENTS);
|
|
||||||
$display("[MULTI_SEG_FIXED] Overlap: %d samples, Advance: %d samples",
|
|
||||||
OVERLAP_SAMPLES, SEGMENT_ADVANCE);
|
|
||||||
`endif
|
`endif
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ST_WAIT_LISTEN: begin
|
||||||
|
// Skip TX chirp duration — count ddc_valid pulses until the
|
||||||
|
// listen window opens. This ensures we only collect echo data,
|
||||||
|
// not TX leakage or dead time.
|
||||||
|
if (ddc_valid) begin
|
||||||
|
if (listen_delay_count >= listen_delay_target - 1) begin
|
||||||
|
// Listen window is now open — begin data collection
|
||||||
|
state <= ST_COLLECT_DATA;
|
||||||
|
`ifdef SIMULATION
|
||||||
|
$display("[MULTI_SEG_FIXED] Listen window open after %0d TX samples, starting %s chirp collection",
|
||||||
|
listen_delay_count + 1,
|
||||||
|
use_long_chirp ? "LONG" : "SHORT");
|
||||||
|
$display("[MULTI_SEG_FIXED] Overlap: %d samples, Advance: %d samples",
|
||||||
|
OVERLAP_SAMPLES, SEGMENT_ADVANCE);
|
||||||
|
`endif
|
||||||
|
end else begin
|
||||||
|
listen_delay_count <= listen_delay_count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
ST_COLLECT_DATA: begin
|
ST_COLLECT_DATA: begin
|
||||||
// Collect samples for current segment with overlap-save
|
// Collect samples for current segment with overlap-save
|
||||||
if (ddc_valid && buffer_write_ptr < BUFFER_SIZE) begin
|
if (ddc_valid && buffer_write_ptr < BUFFER_SIZE) begin
|
||||||
@@ -534,9 +574,36 @@ always @(posedge clk or negedge reset_n) begin
|
|||||||
end
|
end
|
||||||
`endif
|
`endif
|
||||||
|
|
||||||
// ========== OUTPUT CONNECTIONS ==========
|
// ========== OUTPUT CONNECTIONS — OVERLAP-SAVE TRIM ==========
|
||||||
|
// In overlap-save processing, the first OVERLAP_SAMPLES (128) output bins
|
||||||
|
// of each segment after segment 0 are corrupted by circular convolution
|
||||||
|
// wrap-around. These must be discarded. Only the SEGMENT_ADVANCE (896)
|
||||||
|
// valid bins per segment are forwarded downstream.
|
||||||
|
//
|
||||||
|
// For segment 0: all 1024 output bins are valid (no prior overlap).
|
||||||
|
// For segments 1+: bins [0..127] are artifacts, bins [128..1023] are valid.
|
||||||
|
//
|
||||||
|
// We count fft_pc_valid pulses per segment and suppress output during
|
||||||
|
// the overlap region.
|
||||||
|
reg [10:0] output_bin_count;
|
||||||
|
wire output_in_overlap = (current_segment != 0) &&
|
||||||
|
(output_bin_count < OVERLAP_SAMPLES);
|
||||||
|
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n) begin
|
||||||
|
output_bin_count <= 0;
|
||||||
|
end else begin
|
||||||
|
if (state == ST_PROCESSING && buffer_read_ptr == 0) begin
|
||||||
|
// Reset counter at start of each segment's processing
|
||||||
|
output_bin_count <= 0;
|
||||||
|
end else if (fft_pc_valid) begin
|
||||||
|
output_bin_count <= output_bin_count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
assign pc_i_w = fft_pc_i;
|
assign pc_i_w = fft_pc_i;
|
||||||
assign pc_q_w = fft_pc_q;
|
assign pc_q_w = fft_pc_q;
|
||||||
assign pc_valid_w = fft_pc_valid;
|
assign pc_valid_w = fft_pc_valid & ~output_in_overlap;
|
||||||
|
|
||||||
endmodule
|
endmodule
|
||||||
@@ -526,6 +526,25 @@ run_test "Radar Mode Controller" \
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# ===========================================================================
|
||||||
|
# PHASE 5: P0 ADVERSARIAL TESTS — Invariant Violation Fixes
|
||||||
|
# ===========================================================================
|
||||||
|
echo "--- PHASE 5: P0 Adversarial Tests ---"
|
||||||
|
|
||||||
|
run_test "P0 Fix #1: Async FIFO CDC (show-ahead, overflow, reset)" \
|
||||||
|
tb/tb_p0_async_fifo.vvp \
|
||||||
|
tb/tb_p0_async_fifo.v cdc_modules.v
|
||||||
|
|
||||||
|
run_test "P0 Fixes #2/#3/#4: Matched Filter (toggle, listen, overlap)" \
|
||||||
|
tb/tb_p0_mf_adversarial.vvp \
|
||||||
|
tb/tb_p0_mf_adversarial.v matched_filter_multi_segment.v
|
||||||
|
|
||||||
|
run_test "P0 Fix #7: Frame Complete Pulse (falling-edge)" \
|
||||||
|
tb/tb_p0_frame_pulse.vvp \
|
||||||
|
tb/tb_p0_frame_pulse.v
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
# SUMMARY
|
# SUMMARY
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
@@ -291,9 +291,12 @@ class Mixer:
|
|||||||
Convert 8-bit unsigned ADC to 18-bit signed.
|
Convert 8-bit unsigned ADC to 18-bit signed.
|
||||||
RTL: adc_signed_w = {1'b0, adc_data, {9{1'b0}}} -
|
RTL: adc_signed_w = {1'b0, adc_data, {9{1'b0}}} -
|
||||||
{1'b0, {8{1'b1}}, {9{1'b0}}} / 2
|
{1'b0, {8{1'b1}}, {9{1'b0}}} / 2
|
||||||
= (adc_data << 9) - (0xFF << 9) / 2
|
|
||||||
= (adc_data << 9) - (0xFF << 8) [integer division]
|
Verilog '/' binds tighter than '-', so the division applies
|
||||||
= (adc_data << 9) - 0x7F80
|
only to the second concatenation:
|
||||||
|
{1'b0, 8'hFF, 9'b0} = 0x1FE00
|
||||||
|
0x1FE00 / 2 = 0xFF00 = 65280
|
||||||
|
Result: (adc_data << 9) - 0xFF00
|
||||||
"""
|
"""
|
||||||
adc_data_8bit = adc_data_8bit & 0xFF
|
adc_data_8bit = adc_data_8bit & 0xFF
|
||||||
# {1'b0, adc_data, 9'b0} = adc_data << 9, zero-padded to 18 bits
|
# {1'b0, adc_data, 9'b0} = adc_data << 9, zero-padded to 18 bits
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ def run_ddc(adc_samples):
|
|||||||
for n in range(n_samples):
|
for n in range(n_samples):
|
||||||
# ADC sign conversion: RTL does offset binary → signed 18-bit
|
# ADC sign conversion: RTL does offset binary → signed 18-bit
|
||||||
# adc_signed_w = {1'b0, adc_data, 9'b0} - {1'b0, 8'hFF, 9'b0}/2
|
# adc_signed_w = {1'b0, adc_data, 9'b0} - {1'b0, 8'hFF, 9'b0}/2
|
||||||
# Simplified: center around zero, scale to 18-bit
|
# Exact: (adc_val << 9) - 0xFF00, where 0xFF00 = {1'b0,8'hFF,9'b0}/2
|
||||||
adc_val = int(adc_samples[n])
|
adc_val = int(adc_samples[n])
|
||||||
adc_signed = (adc_val - 128) << 9 # Approximate RTL sign conversion to 18-bit
|
adc_signed = (adc_val << 9) - 0xFF00 # Exact RTL: {1'b0,adc,9'b0} - {1'b0,8'hFF,9'b0}/2
|
||||||
adc_signed = saturate(adc_signed, 18)
|
adc_signed = saturate(adc_signed, 18)
|
||||||
|
|
||||||
# NCO lookup (ignoring dithering for golden reference)
|
# NCO lookup (ignoring dithering for golden reference)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,558 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ADVERSARIAL TESTBENCH: cdc_async_fifo (P0 Fix #1)
|
||||||
|
// ============================================================================
|
||||||
|
// Actively tries to BREAK the async FIFO that replaced the flawed
|
||||||
|
// Gray-encoded CDC for the DDC 400→100 MHz sample path.
|
||||||
|
//
|
||||||
|
// Attack vectors:
|
||||||
|
// 1. Read on empty FIFO — no spurious rd_valid
|
||||||
|
// 2. Single write/read — basic data integrity
|
||||||
|
// 3. Fill to capacity — wr_full asserts correctly
|
||||||
|
// 4. Overflow — write-when-full must be rejected, no corruption
|
||||||
|
// 5. Ordered streaming — FIFO order preserved under sustained load
|
||||||
|
// 6. Reset mid-transfer — clean recovery, no stale data
|
||||||
|
// 7. Burst writes at max wr_clk rate — stress back-pressure
|
||||||
|
// 8. wr_full deasserts promptly after read
|
||||||
|
// 9. Alternating single-entry traffic — throughput = 1
|
||||||
|
// 10. Pathological data patterns — all-ones, alternating bits
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
module tb_p0_async_fifo;
|
||||||
|
|
||||||
|
localparam WR_PERIOD = 2.5; // 400 MHz source clock
|
||||||
|
localparam RD_PERIOD = 10.0; // 100 MHz destination clock
|
||||||
|
localparam WIDTH = 18;
|
||||||
|
localparam DEPTH = 8;
|
||||||
|
|
||||||
|
// ── Test bookkeeping ─────────────────────────────────────
|
||||||
|
integer pass_count = 0;
|
||||||
|
integer fail_count = 0;
|
||||||
|
integer test_num = 0;
|
||||||
|
integer i, j;
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// ── DUT signals ──────────────────────────────────────────
|
||||||
|
reg wr_clk = 0;
|
||||||
|
reg rd_clk = 0;
|
||||||
|
reg wr_reset_n = 0;
|
||||||
|
reg rd_reset_n = 0;
|
||||||
|
reg [WIDTH-1:0] wr_data = 0;
|
||||||
|
reg wr_en = 0;
|
||||||
|
wire wr_full;
|
||||||
|
wire [WIDTH-1:0] rd_data;
|
||||||
|
wire rd_valid;
|
||||||
|
reg rd_ack = 0;
|
||||||
|
|
||||||
|
always #(WR_PERIOD/2) wr_clk = ~wr_clk;
|
||||||
|
always #(RD_PERIOD/2) rd_clk = ~rd_clk;
|
||||||
|
|
||||||
|
cdc_async_fifo #(
|
||||||
|
.WIDTH(WIDTH), .DEPTH(DEPTH), .ADDR_BITS(3)
|
||||||
|
) dut (
|
||||||
|
.wr_clk(wr_clk), .wr_reset_n(wr_reset_n),
|
||||||
|
.wr_data(wr_data), .wr_en(wr_en), .wr_full(wr_full),
|
||||||
|
.rd_clk(rd_clk), .rd_reset_n(rd_reset_n),
|
||||||
|
.rd_data(rd_data), .rd_valid(rd_valid), .rd_ack(rd_ack)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Helper tasks ─────────────────────────────────────────
|
||||||
|
task do_reset;
|
||||||
|
begin
|
||||||
|
wr_en = 0; rd_ack = 0; wr_data = 0;
|
||||||
|
wr_reset_n = 0; rd_reset_n = 0;
|
||||||
|
#100;
|
||||||
|
wr_reset_n = 1; rd_reset_n = 1;
|
||||||
|
#50;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task wait_wr_n;
|
||||||
|
input integer n;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
for (k = 0; k < n; k = k + 1) @(posedge wr_clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task wait_rd_n;
|
||||||
|
input integer n;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
for (k = 0; k < n; k = k + 1) @(posedge rd_clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ── Read one entry with timeout ──────────────────────────
|
||||||
|
reg [WIDTH-1:0] read_result;
|
||||||
|
reg read_ok;
|
||||||
|
|
||||||
|
task read_one;
|
||||||
|
output [WIDTH-1:0] data_out;
|
||||||
|
output valid_out;
|
||||||
|
integer timeout;
|
||||||
|
begin
|
||||||
|
rd_ack = 1;
|
||||||
|
valid_out = 0;
|
||||||
|
data_out = {WIDTH{1'bx}};
|
||||||
|
for (timeout = 0; timeout < 20; timeout = timeout + 1) begin
|
||||||
|
@(posedge rd_clk);
|
||||||
|
if (rd_valid) begin
|
||||||
|
data_out = rd_data;
|
||||||
|
valid_out = 1;
|
||||||
|
timeout = 999; // break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
@(posedge rd_clk);
|
||||||
|
rd_ack = 0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ── Drain FIFO, return count of entries read ─────────────
|
||||||
|
integer drain_count;
|
||||||
|
reg [WIDTH-1:0] drain_buf [0:15];
|
||||||
|
|
||||||
|
task drain_fifo;
|
||||||
|
output integer count;
|
||||||
|
integer t;
|
||||||
|
begin
|
||||||
|
count = 0;
|
||||||
|
rd_ack = 1;
|
||||||
|
for (t = 0; t < 60; t = t + 1) begin
|
||||||
|
@(posedge rd_clk);
|
||||||
|
if (rd_valid && count < 16) begin
|
||||||
|
drain_buf[count] = rd_data;
|
||||||
|
count = count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
rd_ack = 0;
|
||||||
|
wait_rd_n(3);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// MAIN TEST SEQUENCE
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
initial begin
|
||||||
|
$dumpfile("tb_p0_async_fifo.vcd");
|
||||||
|
$dumpvars(0, tb_p0_async_fifo);
|
||||||
|
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 1: Empty FIFO — no spurious rd_valid
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 1: Empty FIFO behavior ===");
|
||||||
|
|
||||||
|
// 1a: rd_valid must be 0 when nothing written
|
||||||
|
wait_rd_n(10);
|
||||||
|
check(rd_valid == 0, "Empty FIFO: rd_valid is 0 (no writes)");
|
||||||
|
|
||||||
|
// 1b: rd_ack on empty must not produce spurious valid
|
||||||
|
rd_ack = 1;
|
||||||
|
wait_rd_n(10);
|
||||||
|
check(rd_valid == 0, "Empty FIFO: rd_ack on empty produces no valid");
|
||||||
|
rd_ack = 0;
|
||||||
|
wait_rd_n(3);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 2: Single write/read
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 2: Single write/read ===");
|
||||||
|
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = 18'h2ABCD;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Wait for CDC propagation
|
||||||
|
wait_rd_n(6);
|
||||||
|
check(rd_valid == 1, "Single write: rd_valid asserted");
|
||||||
|
check(rd_data == 18'h2ABCD, "Single write: data integrity");
|
||||||
|
|
||||||
|
// ACK and verify deassert
|
||||||
|
#1; rd_ack = 1;
|
||||||
|
@(posedge rd_clk); #1;
|
||||||
|
rd_ack = 0;
|
||||||
|
wait_rd_n(6);
|
||||||
|
check(rd_valid == 0, "Single write: rd_valid deasserts after ack+empty");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 3: Fill to capacity
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// NOTE: This FIFO uses a pre-fetch show-ahead architecture.
|
||||||
|
// When the FIFO goes from empty to non-empty, the read domain
|
||||||
|
// auto-presents the first entry into rd_data_reg, advancing
|
||||||
|
// rd_ptr by 1. This frees one slot in the underlying memory,
|
||||||
|
// so wr_full requires DEPTH+1 writes (DEPTH in mem + 1 in the
|
||||||
|
// output register). This is necessary because a combinational
|
||||||
|
// read from mem across clock domains would be CDC-unsafe.
|
||||||
|
$display("\n=== GROUP 3: Fill to capacity ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// Write DEPTH entries
|
||||||
|
for (i = 0; i < DEPTH; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = i[17:0] + 18'h100;
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Wait for auto-present round-trip through both synchronizers
|
||||||
|
wait_wr_n(12);
|
||||||
|
|
||||||
|
// After auto-present, rd_ptr advanced by 1 → 1 slot freed → not full yet
|
||||||
|
check(wr_full == 0, "Pre-fetch show-ahead: DEPTH writes, 1 auto-present frees slot");
|
||||||
|
|
||||||
|
// Write one more entry into the freed slot → now truly full
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = 18'hFACE;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
wait_wr_n(6);
|
||||||
|
check(wr_full == 1, "Fill-to-full: wr_full asserted after DEPTH+1 writes");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 4: Overflow — write when full
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 4: Overflow protection ===");
|
||||||
|
|
||||||
|
// Attempt to write 3 more entries while full
|
||||||
|
for (i = 0; i < 3; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = 18'h3DEAD + i[17:0];
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Drain and verify DEPTH+1 entries (DEPTH mem + 1 output register)
|
||||||
|
drain_fifo(drain_count);
|
||||||
|
check(drain_count == DEPTH + 1, "Overflow: exactly DEPTH+1 entries (overflow rejected)");
|
||||||
|
|
||||||
|
// Verify data integrity — check first DEPTH entries + the extra FACE entry
|
||||||
|
begin : overflow_data_check
|
||||||
|
reg data_ok;
|
||||||
|
data_ok = 1;
|
||||||
|
// First entry is the auto-presented one (index 0 from Group 3)
|
||||||
|
if (drain_buf[0] !== 18'h100) begin
|
||||||
|
$display(" overflow corruption at [0]: expected %h, got %h",
|
||||||
|
18'h100, drain_buf[0]);
|
||||||
|
data_ok = 0;
|
||||||
|
end
|
||||||
|
// Next DEPTH-1 entries are indices 1..DEPTH-1
|
||||||
|
for (i = 1; i < DEPTH; i = i + 1) begin
|
||||||
|
if (drain_buf[i] !== i[17:0] + 18'h100) begin
|
||||||
|
$display(" overflow corruption at [%0d]: expected %h, got %h",
|
||||||
|
i, i[17:0] + 18'h100, drain_buf[i]);
|
||||||
|
data_ok = 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
// Last entry is the FACE entry from the +1 write
|
||||||
|
if (drain_buf[DEPTH] !== 18'hFACE) begin
|
||||||
|
$display(" overflow corruption at [%0d]: expected %h, got %h",
|
||||||
|
DEPTH, 18'hFACE, drain_buf[DEPTH]);
|
||||||
|
data_ok = 0;
|
||||||
|
end
|
||||||
|
check(data_ok, "Overflow: all DEPTH+1 entries data intact (no corruption)");
|
||||||
|
end
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 5: Data ordering under sustained streaming
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 5: Sustained streaming order ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// Simulate CIC-decimated DDC output: 1 sample per 4 wr_clks
|
||||||
|
// Reader continuously ACKs (rate-matched at 100 MHz)
|
||||||
|
begin : stream_test
|
||||||
|
reg [WIDTH-1:0] expected_val;
|
||||||
|
integer read_idx;
|
||||||
|
reg ordering_ok;
|
||||||
|
|
||||||
|
ordering_ok = 1;
|
||||||
|
read_idx = 0;
|
||||||
|
|
||||||
|
fork
|
||||||
|
// Writer: 32 samples, 1 per 4 wr_clks (rate-matched to rd_clk)
|
||||||
|
begin : stream_writer
|
||||||
|
integer w;
|
||||||
|
for (w = 0; w < 32; w = w + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = w[17:0] + 18'h1000;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
wait_wr_n(2); // 4 wr_clks total per sample
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
// Reader: continuously consume at rd_clk rate
|
||||||
|
begin : stream_reader
|
||||||
|
integer rd_t;
|
||||||
|
rd_ack = 1;
|
||||||
|
for (rd_t = 0; rd_t < 500 && read_idx < 32; rd_t = rd_t + 1) begin
|
||||||
|
@(posedge rd_clk);
|
||||||
|
if (rd_valid) begin
|
||||||
|
expected_val = read_idx[17:0] + 18'h1000;
|
||||||
|
if (rd_data !== expected_val) begin
|
||||||
|
$display(" stream order error at [%0d]: expected %h, got %h",
|
||||||
|
read_idx, expected_val, rd_data);
|
||||||
|
ordering_ok = 0;
|
||||||
|
end
|
||||||
|
read_idx = read_idx + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
#1; rd_ack = 0;
|
||||||
|
end
|
||||||
|
join
|
||||||
|
|
||||||
|
check(read_idx == 32, "Streaming: all 32 samples received");
|
||||||
|
check(ordering_ok, "Streaming: FIFO order preserved");
|
||||||
|
end
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 6: Reset mid-transfer
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 6: Reset mid-transfer ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// Write 4 entries
|
||||||
|
for (i = 0; i < 4; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = i[17:0] + 18'hAA00;
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
wait_wr_n(3);
|
||||||
|
|
||||||
|
// Assert reset while data is in FIFO
|
||||||
|
wr_reset_n = 0; rd_reset_n = 0;
|
||||||
|
#50;
|
||||||
|
wr_reset_n = 1; rd_reset_n = 1;
|
||||||
|
#50;
|
||||||
|
|
||||||
|
// 6a: FIFO must be empty after reset
|
||||||
|
wait_rd_n(10);
|
||||||
|
check(rd_valid == 0, "Reset mid-xfer: FIFO empty (no stale data)");
|
||||||
|
check(wr_full == 0, "Reset mid-xfer: wr_full deasserted");
|
||||||
|
|
||||||
|
// 6b: New write after reset must work
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = 18'h3CAFE;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
wait_rd_n(6);
|
||||||
|
check(rd_valid == 1, "Reset recovery: rd_valid for new write");
|
||||||
|
check(rd_data == 18'h3CAFE, "Reset recovery: correct data");
|
||||||
|
#1; rd_ack = 1; @(posedge rd_clk); #1; rd_ack = 0;
|
||||||
|
wait_rd_n(5);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 7: Burst writes at max wr_clk rate
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 7: Max-rate burst ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// Write 7 entries back-to-back (1 per wr_clk, no decimation)
|
||||||
|
for (i = 0; i < 7; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = i[17:0] + 18'hB000;
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Drain and count
|
||||||
|
drain_fifo(drain_count);
|
||||||
|
check(drain_count == 7, "Burst: all 7 entries received (no drops)");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 8: wr_full deasserts after read
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 8: wr_full release ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// Fill FIFO: DEPTH entries first
|
||||||
|
for (i = 0; i < DEPTH; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = i[17:0];
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Wait for auto-present round-trip
|
||||||
|
wait_wr_n(12);
|
||||||
|
|
||||||
|
// Write the +1 entry (into the slot freed by auto-present)
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = 18'h3BEEF;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
wait_wr_n(6);
|
||||||
|
check(wr_full == 1, "wr_full release: initially full (DEPTH+1 writes)");
|
||||||
|
|
||||||
|
// Read one entry (ACK the auto-presented data)
|
||||||
|
#1; rd_ack = 1;
|
||||||
|
wait_rd_n(2);
|
||||||
|
#1; rd_ack = 0;
|
||||||
|
|
||||||
|
// Wait for rd_ptr sync back to wr domain (2 wr_clk cycles + margin)
|
||||||
|
wait_wr_n(10);
|
||||||
|
check(wr_full == 0, "wr_full release: deasserts after 1 read");
|
||||||
|
|
||||||
|
// Drain rest
|
||||||
|
drain_fifo(drain_count);
|
||||||
|
wait_rd_n(5);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 9: Alternating single-entry throughput
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 9: Alternating single-entry ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
begin : alt_test
|
||||||
|
reg alt_ok;
|
||||||
|
reg alt_got_valid;
|
||||||
|
integer rd_w;
|
||||||
|
alt_ok = 1;
|
||||||
|
for (i = 0; i < 12; i = i + 1) begin
|
||||||
|
// Write 1
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = i[17:0] + 18'hC000;
|
||||||
|
wr_en = 1;
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Read 1 — wait for auto-present with rd_ack=0, then pulse ack
|
||||||
|
rd_ack = 0;
|
||||||
|
alt_got_valid = 0;
|
||||||
|
for (rd_w = 0; rd_w < 20; rd_w = rd_w + 1) begin
|
||||||
|
@(posedge rd_clk);
|
||||||
|
if (rd_valid && !alt_got_valid) begin
|
||||||
|
alt_got_valid = 1;
|
||||||
|
if (rd_data !== i[17:0] + 18'hC000) begin
|
||||||
|
$display(" alt[%0d]: data mismatch", i);
|
||||||
|
alt_ok = 0;
|
||||||
|
end
|
||||||
|
rd_w = 999; // break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (!alt_got_valid) begin
|
||||||
|
$display(" alt[%0d]: no rd_valid after write", i);
|
||||||
|
alt_ok = 0;
|
||||||
|
end
|
||||||
|
// Consume the entry
|
||||||
|
#1; rd_ack = 1;
|
||||||
|
@(posedge rd_clk); #1;
|
||||||
|
rd_ack = 0;
|
||||||
|
wait_rd_n(2);
|
||||||
|
end
|
||||||
|
check(alt_ok, "Alternating: 12 single-entry cycles all correct");
|
||||||
|
end
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP 10: Pathological data patterns
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP 10: Pathological data patterns ===");
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
begin : patho_test
|
||||||
|
reg patho_ok;
|
||||||
|
reg patho_seen;
|
||||||
|
reg [WIDTH-1:0] patterns [0:4];
|
||||||
|
integer rd_w;
|
||||||
|
patterns[0] = 18'h3FFFF; // all ones
|
||||||
|
patterns[1] = 18'h00000; // all zeros
|
||||||
|
patterns[2] = 18'h2AAAA; // alternating 10...
|
||||||
|
patterns[3] = 18'h15555; // alternating 01...
|
||||||
|
patterns[4] = 18'h20001; // MSB + LSB set
|
||||||
|
|
||||||
|
patho_ok = 1;
|
||||||
|
// Write all 5 patterns
|
||||||
|
for (i = 0; i < 5; i = i + 1) begin
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_data = patterns[i];
|
||||||
|
wr_en = 1;
|
||||||
|
end
|
||||||
|
@(posedge wr_clk); #1;
|
||||||
|
wr_en = 0;
|
||||||
|
|
||||||
|
// Read one at a time: wait for auto-present, check, ack
|
||||||
|
rd_ack = 0;
|
||||||
|
for (i = 0; i < 5; i = i + 1) begin
|
||||||
|
patho_seen = 0;
|
||||||
|
for (rd_w = 0; rd_w < 30; rd_w = rd_w + 1) begin
|
||||||
|
@(posedge rd_clk);
|
||||||
|
if (rd_valid && !patho_seen) begin
|
||||||
|
patho_seen = 1;
|
||||||
|
if (rd_data !== patterns[i]) begin
|
||||||
|
$display(" pattern[%0d]: expected %h got %h",
|
||||||
|
i, patterns[i], rd_data);
|
||||||
|
patho_ok = 0;
|
||||||
|
end
|
||||||
|
rd_w = 999; // break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if (!patho_seen) begin
|
||||||
|
$display(" pattern[%0d]: no valid", i);
|
||||||
|
patho_ok = 0;
|
||||||
|
end
|
||||||
|
// Consume the entry
|
||||||
|
#1; rd_ack = 1;
|
||||||
|
@(posedge rd_clk); #1;
|
||||||
|
rd_ack = 0;
|
||||||
|
end
|
||||||
|
check(patho_ok, "Pathological: all 5 bit-patterns survive CDC");
|
||||||
|
end
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// SUMMARY
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
$display("\n============================================");
|
||||||
|
$display(" P0 Fix #1: Async FIFO Adversarial Tests");
|
||||||
|
$display("============================================");
|
||||||
|
$display(" PASSED: %0d", pass_count);
|
||||||
|
$display(" FAILED: %0d", fail_count);
|
||||||
|
$display("============================================");
|
||||||
|
|
||||||
|
if (fail_count > 0)
|
||||||
|
$display("RESULT: FAIL");
|
||||||
|
else
|
||||||
|
$display("RESULT: PASS");
|
||||||
|
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Timeout watchdog
|
||||||
|
initial begin
|
||||||
|
#1000000;
|
||||||
|
$display("[FAIL] TIMEOUT: simulation exceeded 1ms");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,361 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ADVERSARIAL TESTBENCH: frame_complete Pulse Width (P0 Fix #7)
|
||||||
|
// ============================================================================
|
||||||
|
// Tests the falling-edge pulse detection pattern used in doppler_processor.v
|
||||||
|
// (lines 533-551) for the frame_complete signal.
|
||||||
|
//
|
||||||
|
// The OLD code held frame_complete as a continuous level whenever the
|
||||||
|
// Doppler processor was idle. This caused the AGC (rx_gain_control) to
|
||||||
|
// re-evaluate every clock with zeroed accumulators, collapsing gain control.
|
||||||
|
//
|
||||||
|
// The FIX detects the falling edge of processing_active:
|
||||||
|
// assign processing_active = (state != S_IDLE);
|
||||||
|
// reg processing_active_prev;
|
||||||
|
// always @(posedge clk or negedge reset_n)
|
||||||
|
// processing_active_prev <= processing_active;
|
||||||
|
// assign frame_complete = (~processing_active & processing_active_prev);
|
||||||
|
//
|
||||||
|
// This DUT wrapper replicates the EXACT pattern from doppler_processor.v.
|
||||||
|
// The adversarial tests drive the state input and verify:
|
||||||
|
// - Pulse width is EXACTLY 1 clock cycle
|
||||||
|
// - No pulse during extended idle
|
||||||
|
// - No pulse on reset deassertion
|
||||||
|
// - Back-to-back frame completions produce distinct pulses
|
||||||
|
// - State transitions not touching S_IDLE produce no pulse
|
||||||
|
// - OLD behavior (continuous level) is regressed
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ── DUT: Exact replica of doppler_processor.v frame_complete logic ──
|
||||||
|
module frame_complete_dut (
|
||||||
|
input wire clk,
|
||||||
|
input wire reset_n,
|
||||||
|
input wire [3:0] state, // Mimic doppler FSM state input
|
||||||
|
output wire processing_active,
|
||||||
|
output wire frame_complete
|
||||||
|
);
|
||||||
|
// S_IDLE encoding from doppler_processor_optimized
|
||||||
|
localparam [3:0] S_IDLE = 4'd0;
|
||||||
|
|
||||||
|
assign processing_active = (state != S_IDLE);
|
||||||
|
|
||||||
|
reg processing_active_prev;
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n)
|
||||||
|
processing_active_prev <= 1'b0;
|
||||||
|
else
|
||||||
|
processing_active_prev <= processing_active;
|
||||||
|
end
|
||||||
|
|
||||||
|
assign frame_complete = (~processing_active & processing_active_prev);
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
|
||||||
|
// ── TESTBENCH ────────────────────────────────────────────────
|
||||||
|
module tb_p0_frame_pulse;
|
||||||
|
|
||||||
|
localparam CLK_PERIOD = 10.0; // 100 MHz
|
||||||
|
|
||||||
|
// Doppler FSM state encodings (from doppler_processor_optimized)
|
||||||
|
localparam [3:0] S_IDLE = 4'd0;
|
||||||
|
localparam [3:0] S_ACCUMULATE = 4'd1;
|
||||||
|
localparam [3:0] S_WINDOW = 4'd2;
|
||||||
|
localparam [3:0] S_FFT = 4'd3;
|
||||||
|
localparam [3:0] S_OUTPUT = 4'd4;
|
||||||
|
localparam [3:0] S_NEXT_BIN = 4'd5;
|
||||||
|
|
||||||
|
// ── Test bookkeeping ─────────────────────────────────────
|
||||||
|
integer pass_count = 0;
|
||||||
|
integer fail_count = 0;
|
||||||
|
integer test_num = 0;
|
||||||
|
integer i;
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// ── DUT signals ──────────────────────────────────────────
|
||||||
|
reg clk = 0;
|
||||||
|
reg reset_n = 0;
|
||||||
|
reg [3:0] state = S_IDLE;
|
||||||
|
wire processing_active;
|
||||||
|
wire frame_complete;
|
||||||
|
|
||||||
|
always #(CLK_PERIOD/2) clk = ~clk;
|
||||||
|
|
||||||
|
frame_complete_dut dut (
|
||||||
|
.clk(clk),
|
||||||
|
.reset_n(reset_n),
|
||||||
|
.state(state),
|
||||||
|
.processing_active(processing_active),
|
||||||
|
.frame_complete(frame_complete)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Helper ───────────────────────────────────────────────
|
||||||
|
task wait_n;
|
||||||
|
input integer n;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
for (k = 0; k < n; k = k + 1) @(posedge clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ── Count frame_complete pulses over N clocks ────────────
|
||||||
|
integer pulse_count;
|
||||||
|
|
||||||
|
task count_pulses;
|
||||||
|
input integer n_clocks;
|
||||||
|
output integer count;
|
||||||
|
integer c;
|
||||||
|
begin
|
||||||
|
count = 0;
|
||||||
|
for (c = 0; c < n_clocks; c = c + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) count = count + 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// MAIN TEST SEQUENCE
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
initial begin
|
||||||
|
$dumpfile("tb_p0_frame_pulse.vcd");
|
||||||
|
$dumpvars(0, tb_p0_frame_pulse);
|
||||||
|
|
||||||
|
// ── RESET ────────────────────────────────────────────
|
||||||
|
state = S_IDLE;
|
||||||
|
reset_n = 0;
|
||||||
|
#100;
|
||||||
|
reset_n = 1;
|
||||||
|
@(posedge clk);
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 1: No pulse on reset deassertion
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 1: Reset deassertion ===");
|
||||||
|
// processing_active = 0 (state = S_IDLE)
|
||||||
|
// processing_active_prev was reset to 0
|
||||||
|
// frame_complete = ~0 & 0 = 0
|
||||||
|
check(frame_complete == 0, "No pulse on reset deassertion (both 0)");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 2: No pulse during extended idle
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 2: Extended idle ===");
|
||||||
|
count_pulses(200, pulse_count);
|
||||||
|
check(pulse_count == 0, "No pulse during 200 clocks of continuous idle");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 3: Single frame completion — pulse width = 1
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 3: Single frame completion ===");
|
||||||
|
|
||||||
|
// Enter active state
|
||||||
|
@(posedge clk); #1;
|
||||||
|
state = S_ACCUMULATE;
|
||||||
|
wait_n(5);
|
||||||
|
check(processing_active == 1, "Active: processing_active = 1");
|
||||||
|
check(frame_complete == 0, "Active: no frame_complete while active");
|
||||||
|
|
||||||
|
// Stay active for 50 clocks (various states)
|
||||||
|
#1; state = S_WINDOW; wait_n(10);
|
||||||
|
#1; state = S_FFT; wait_n(10);
|
||||||
|
#1; state = S_OUTPUT; wait_n(10);
|
||||||
|
#1; state = S_NEXT_BIN; wait_n(10);
|
||||||
|
check(frame_complete == 0, "Active (multi-state): no frame_complete");
|
||||||
|
|
||||||
|
// Return to idle — should produce exactly 1 pulse
|
||||||
|
#1; state = S_IDLE;
|
||||||
|
@(posedge clk);
|
||||||
|
// On this edge: processing_active = 0, processing_active_prev = 1
|
||||||
|
// frame_complete = ~0 & 1 = 1
|
||||||
|
check(frame_complete == 1, "Completion: frame_complete fires");
|
||||||
|
|
||||||
|
@(posedge clk);
|
||||||
|
// Now: processing_active_prev catches up to 0
|
||||||
|
// frame_complete = ~0 & 0 = 0
|
||||||
|
check(frame_complete == 0, "Completion: pulse is EXACTLY 1 cycle wide");
|
||||||
|
|
||||||
|
// Verify no more pulses
|
||||||
|
count_pulses(100, pulse_count);
|
||||||
|
check(pulse_count == 0, "Post-completion: no re-fire during idle");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 4: Back-to-back frame completions
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 4: Back-to-back completions ===");
|
||||||
|
|
||||||
|
begin : backtoback_test
|
||||||
|
integer total_pulses;
|
||||||
|
total_pulses = 0;
|
||||||
|
|
||||||
|
// Do 5 rapid frame cycles
|
||||||
|
for (i = 0; i < 5; i = i + 1) begin
|
||||||
|
// Go active
|
||||||
|
@(posedge clk); #1;
|
||||||
|
state = S_ACCUMULATE;
|
||||||
|
wait_n(3);
|
||||||
|
|
||||||
|
// Return to idle
|
||||||
|
#1; state = S_IDLE;
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) total_pulses = total_pulses + 1;
|
||||||
|
@(posedge clk); // pulse should be gone
|
||||||
|
if (frame_complete) begin
|
||||||
|
$display(" [WARN] frame %0d: pulse persisted > 1 cycle", i);
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
check(total_pulses == 5, "Back-to-back: exactly 5 pulses for 5 completions");
|
||||||
|
end
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 5: State transitions not touching S_IDLE
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 5: Non-idle transitions ===");
|
||||||
|
|
||||||
|
@(posedge clk); #1;
|
||||||
|
state = S_ACCUMULATE;
|
||||||
|
wait_n(3);
|
||||||
|
|
||||||
|
// Cycle through active states without returning to idle
|
||||||
|
begin : nonidle_test
|
||||||
|
integer nonidle_pulses;
|
||||||
|
nonidle_pulses = 0;
|
||||||
|
|
||||||
|
#1; state = S_WINDOW;
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) nonidle_pulses = nonidle_pulses + 1;
|
||||||
|
|
||||||
|
#1; state = S_FFT;
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) nonidle_pulses = nonidle_pulses + 1;
|
||||||
|
|
||||||
|
#1; state = S_OUTPUT;
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) nonidle_pulses = nonidle_pulses + 1;
|
||||||
|
|
||||||
|
#1; state = S_NEXT_BIN;
|
||||||
|
@(posedge clk);
|
||||||
|
if (frame_complete) nonidle_pulses = nonidle_pulses + 1;
|
||||||
|
|
||||||
|
#1; state = S_ACCUMULATE;
|
||||||
|
wait_n(10);
|
||||||
|
count_pulses(10, pulse_count);
|
||||||
|
nonidle_pulses = nonidle_pulses + pulse_count;
|
||||||
|
|
||||||
|
check(nonidle_pulses == 0,
|
||||||
|
"Non-idle transitions: zero pulses (all states active)");
|
||||||
|
end
|
||||||
|
|
||||||
|
// Return to idle (one pulse expected)
|
||||||
|
#1; state = S_IDLE;
|
||||||
|
@(posedge clk);
|
||||||
|
check(frame_complete == 1, "Cleanup: pulse on final idle transition");
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 6: Long active period — no premature pulse
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 6: Long active period ===");
|
||||||
|
|
||||||
|
@(posedge clk); #1;
|
||||||
|
state = S_FFT;
|
||||||
|
|
||||||
|
count_pulses(500, pulse_count);
|
||||||
|
check(pulse_count == 0, "Long active (500 clocks): no premature pulse");
|
||||||
|
|
||||||
|
#1; state = S_IDLE;
|
||||||
|
@(posedge clk);
|
||||||
|
check(frame_complete == 1, "Long active → idle: pulse fires");
|
||||||
|
@(posedge clk);
|
||||||
|
check(frame_complete == 0, "Long active → idle: single cycle only");
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 7: Reset during active state
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 7: Reset during active ===");
|
||||||
|
|
||||||
|
@(posedge clk); #1;
|
||||||
|
state = S_ACCUMULATE;
|
||||||
|
wait_n(5);
|
||||||
|
|
||||||
|
// Assert reset while active
|
||||||
|
reset_n = 0;
|
||||||
|
#50;
|
||||||
|
// During reset: processing_active_prev forced to 0
|
||||||
|
// state still = S_ACCUMULATE, processing_active = 1
|
||||||
|
reset_n = 1;
|
||||||
|
@(posedge clk);
|
||||||
|
@(posedge clk);
|
||||||
|
// After reset release: prev = 0, active = 1
|
||||||
|
// frame_complete = ~1 & 0 = 0 (no spurious pulse)
|
||||||
|
check(frame_complete == 0, "Reset during active: no spurious pulse");
|
||||||
|
|
||||||
|
// Now go idle — should pulse
|
||||||
|
#1; state = S_IDLE;
|
||||||
|
@(posedge clk);
|
||||||
|
check(frame_complete == 1, "Reset recovery: pulse on idle after active");
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// TEST 8: REGRESSION — old continuous-level behavior
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== TEST 8: REGRESSION ===");
|
||||||
|
// OLD code: frame_complete = (state == S_IDLE && frame_buffer_full == 0)
|
||||||
|
// This held frame_complete HIGH for the entire idle period.
|
||||||
|
// With AGC sampling frame_complete, this caused re-evaluation every clock.
|
||||||
|
//
|
||||||
|
// The FIX produces a 1-cycle pulse. We've proven:
|
||||||
|
// - Pulse width = 1 cycle (Test 3)
|
||||||
|
// - No re-fire during idle (Test 2, 3)
|
||||||
|
// - Old behavior would have frame_complete = 1 for 200+ clocks (Test 2)
|
||||||
|
//
|
||||||
|
// Quantify: old code would produce 200 "events" over 200 idle clocks.
|
||||||
|
// New code produces 0. This is the fix.
|
||||||
|
|
||||||
|
state = S_IDLE;
|
||||||
|
count_pulses(200, pulse_count);
|
||||||
|
check(pulse_count == 0,
|
||||||
|
"REGRESSION: 0 pulses in 200 idle clocks (old code: 200)");
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// SUMMARY
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
$display("\n============================================");
|
||||||
|
$display(" P0 Fix #7: frame_complete Pulse Tests");
|
||||||
|
$display("============================================");
|
||||||
|
$display(" PASSED: %0d", pass_count);
|
||||||
|
$display(" FAILED: %0d", fail_count);
|
||||||
|
$display("============================================");
|
||||||
|
|
||||||
|
if (fail_count > 0)
|
||||||
|
$display("RESULT: FAIL");
|
||||||
|
else
|
||||||
|
$display("RESULT: PASS");
|
||||||
|
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Timeout watchdog
|
||||||
|
initial begin
|
||||||
|
#500000;
|
||||||
|
$display("[FAIL] TIMEOUT: simulation exceeded 500us");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -0,0 +1,602 @@
|
|||||||
|
`timescale 1ns / 1ps
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ADVERSARIAL TESTBENCH: Matched Filter Fixes (P0 Fixes #2, #3, #4)
|
||||||
|
// ============================================================================
|
||||||
|
// Tests three critical signal-processing invariant fixes in
|
||||||
|
// matched_filter_multi_segment.v:
|
||||||
|
//
|
||||||
|
// Fix #2 — Toggle detection: XOR replaces AND+NOT so both edges of
|
||||||
|
// mc_new_chirp generate chirp_start_pulse (not just 0→1).
|
||||||
|
//
|
||||||
|
// Fix #3 — Listen delay: ST_WAIT_LISTEN state skips TX chirp duration
|
||||||
|
// (counting ddc_valid pulses) before collecting echo samples.
|
||||||
|
//
|
||||||
|
// Fix #4 — Overlap-save trim: First 128 output bins of segments 1+
|
||||||
|
// are suppressed (circular convolution artifacts).
|
||||||
|
//
|
||||||
|
// A STUB processing chain replaces the real FFT pipeline, providing
|
||||||
|
// controlled timing for state machine verification.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// STUB: matched_filter_processing_chain
|
||||||
|
// ============================================================================
|
||||||
|
// Same port signature as the real module. Accepts 1024 adc_valid samples,
|
||||||
|
// simulates a short processing delay, then outputs 1024 range_profile_valid
|
||||||
|
// pulses with incrementing data. chain_state reports 0 when idle.
|
||||||
|
// ============================================================================
|
||||||
|
module matched_filter_processing_chain (
|
||||||
|
input wire clk,
|
||||||
|
input wire reset_n,
|
||||||
|
|
||||||
|
input wire [15:0] adc_data_i,
|
||||||
|
input wire [15:0] adc_data_q,
|
||||||
|
input wire adc_valid,
|
||||||
|
|
||||||
|
input wire [5:0] chirp_counter,
|
||||||
|
|
||||||
|
input wire [15:0] long_chirp_real,
|
||||||
|
input wire [15:0] long_chirp_imag,
|
||||||
|
input wire [15:0] short_chirp_real,
|
||||||
|
input wire [15:0] short_chirp_imag,
|
||||||
|
|
||||||
|
output reg signed [15:0] range_profile_i,
|
||||||
|
output reg signed [15:0] range_profile_q,
|
||||||
|
output reg range_profile_valid,
|
||||||
|
|
||||||
|
output wire [3:0] chain_state
|
||||||
|
);
|
||||||
|
|
||||||
|
localparam [3:0] ST_IDLE = 4'd0;
|
||||||
|
localparam [3:0] ST_COLLECTING = 4'd1;
|
||||||
|
localparam [3:0] ST_DELAY = 4'd2;
|
||||||
|
localparam [3:0] ST_OUTPUTTING = 4'd3;
|
||||||
|
localparam [3:0] ST_DONE = 4'd9;
|
||||||
|
|
||||||
|
reg [3:0] state = ST_IDLE;
|
||||||
|
reg [10:0] count = 0;
|
||||||
|
|
||||||
|
assign chain_state = state;
|
||||||
|
|
||||||
|
always @(posedge clk or negedge reset_n) begin
|
||||||
|
if (!reset_n) begin
|
||||||
|
state <= ST_IDLE;
|
||||||
|
count <= 0;
|
||||||
|
range_profile_valid <= 0;
|
||||||
|
range_profile_i <= 0;
|
||||||
|
range_profile_q <= 0;
|
||||||
|
end else begin
|
||||||
|
range_profile_valid <= 0;
|
||||||
|
|
||||||
|
case (state)
|
||||||
|
ST_IDLE: begin
|
||||||
|
count <= 0;
|
||||||
|
if (adc_valid) begin
|
||||||
|
state <= ST_COLLECTING;
|
||||||
|
count <= 1;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ST_COLLECTING: begin
|
||||||
|
if (adc_valid) begin
|
||||||
|
count <= count + 1;
|
||||||
|
if (count >= 11'd1023) begin
|
||||||
|
state <= ST_DELAY;
|
||||||
|
count <= 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ST_DELAY: begin
|
||||||
|
// Simulate processing latency (8 clocks)
|
||||||
|
count <= count + 1;
|
||||||
|
if (count >= 11'd7) begin
|
||||||
|
state <= ST_OUTPUTTING;
|
||||||
|
count <= 0;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ST_OUTPUTTING: begin
|
||||||
|
range_profile_valid <= 1;
|
||||||
|
range_profile_i <= count[15:0];
|
||||||
|
range_profile_q <= ~count[15:0];
|
||||||
|
count <= count + 1;
|
||||||
|
if (count >= 11'd1023) begin
|
||||||
|
state <= ST_DONE;
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ST_DONE: begin
|
||||||
|
state <= ST_IDLE;
|
||||||
|
end
|
||||||
|
|
||||||
|
default: state <= ST_IDLE;
|
||||||
|
endcase
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// TESTBENCH
|
||||||
|
// ============================================================================
|
||||||
|
module tb_p0_mf_adversarial;
|
||||||
|
|
||||||
|
localparam CLK_PERIOD = 10.0; // 100 MHz
|
||||||
|
|
||||||
|
// Override matched_filter parameters for fast simulation
|
||||||
|
localparam TB_LONG_CHIRP = 2000; // echo samples + listen delay target
|
||||||
|
localparam TB_SHORT_CHIRP = 10;
|
||||||
|
localparam TB_LONG_SEGS = 3;
|
||||||
|
localparam TB_SHORT_SEGS = 1;
|
||||||
|
localparam TB_OVERLAP = 128;
|
||||||
|
localparam TB_BUF_SIZE = 1024;
|
||||||
|
localparam TB_SEG_ADVANCE = TB_BUF_SIZE - TB_OVERLAP; // 896
|
||||||
|
|
||||||
|
// ── Test bookkeeping ─────────────────────────────────────
|
||||||
|
integer pass_count = 0;
|
||||||
|
integer fail_count = 0;
|
||||||
|
integer test_num = 0;
|
||||||
|
integer i;
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// ── DUT signals ──────────────────────────────────────────
|
||||||
|
reg clk = 0;
|
||||||
|
reg reset_n = 0;
|
||||||
|
reg signed [17:0] ddc_i = 0;
|
||||||
|
reg signed [17:0] ddc_q = 0;
|
||||||
|
reg ddc_valid = 0;
|
||||||
|
reg use_long_chirp = 0;
|
||||||
|
reg [5:0] chirp_counter = 0;
|
||||||
|
reg mc_new_chirp = 0;
|
||||||
|
reg mc_new_elevation = 0;
|
||||||
|
reg mc_new_azimuth = 0;
|
||||||
|
reg [15:0] long_chirp_real = 0;
|
||||||
|
reg [15:0] long_chirp_imag = 0;
|
||||||
|
reg [15:0] short_chirp_real = 0;
|
||||||
|
reg [15:0] short_chirp_imag = 0;
|
||||||
|
reg mem_ready = 1; // Always ready (stub memory)
|
||||||
|
|
||||||
|
wire [1:0] segment_request;
|
||||||
|
wire [9:0] sample_addr_out;
|
||||||
|
wire mem_request_w;
|
||||||
|
wire signed [15:0] pc_i_w;
|
||||||
|
wire signed [15:0] pc_q_w;
|
||||||
|
wire pc_valid_w;
|
||||||
|
wire [3:0] status;
|
||||||
|
|
||||||
|
always #(CLK_PERIOD/2) clk = ~clk;
|
||||||
|
|
||||||
|
matched_filter_multi_segment #(
|
||||||
|
.BUFFER_SIZE(TB_BUF_SIZE),
|
||||||
|
.LONG_CHIRP_SAMPLES(TB_LONG_CHIRP),
|
||||||
|
.SHORT_CHIRP_SAMPLES(TB_SHORT_CHIRP),
|
||||||
|
.OVERLAP_SAMPLES(TB_OVERLAP),
|
||||||
|
.SEGMENT_ADVANCE(TB_SEG_ADVANCE),
|
||||||
|
.LONG_SEGMENTS(TB_LONG_SEGS),
|
||||||
|
.SHORT_SEGMENTS(TB_SHORT_SEGS),
|
||||||
|
.DEBUG(0)
|
||||||
|
) dut (
|
||||||
|
.clk(clk),
|
||||||
|
.reset_n(reset_n),
|
||||||
|
.ddc_i(ddc_i),
|
||||||
|
.ddc_q(ddc_q),
|
||||||
|
.ddc_valid(ddc_valid),
|
||||||
|
.use_long_chirp(use_long_chirp),
|
||||||
|
.chirp_counter(chirp_counter),
|
||||||
|
.mc_new_chirp(mc_new_chirp),
|
||||||
|
.mc_new_elevation(mc_new_elevation),
|
||||||
|
.mc_new_azimuth(mc_new_azimuth),
|
||||||
|
.long_chirp_real(long_chirp_real),
|
||||||
|
.long_chirp_imag(long_chirp_imag),
|
||||||
|
.short_chirp_real(short_chirp_real),
|
||||||
|
.short_chirp_imag(short_chirp_imag),
|
||||||
|
.segment_request(segment_request),
|
||||||
|
.sample_addr_out(sample_addr_out),
|
||||||
|
.mem_request(mem_request_w),
|
||||||
|
.mem_ready(mem_ready),
|
||||||
|
.pc_i_w(pc_i_w),
|
||||||
|
.pc_q_w(pc_q_w),
|
||||||
|
.pc_valid_w(pc_valid_w),
|
||||||
|
.status(status)
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Hierarchical refs for observability ──────────────────
|
||||||
|
wire [3:0] dut_state = dut.state;
|
||||||
|
wire dut_chirp_pulse = dut.chirp_start_pulse;
|
||||||
|
wire dut_elev_pulse = dut.elevation_change_pulse;
|
||||||
|
wire dut_azim_pulse = dut.azimuth_change_pulse;
|
||||||
|
wire [15:0] dut_listen_count = dut.listen_delay_count;
|
||||||
|
wire [15:0] dut_listen_target = dut.listen_delay_target;
|
||||||
|
wire [2:0] dut_segment = dut.current_segment;
|
||||||
|
wire [10:0] dut_out_bin_count = dut.output_bin_count;
|
||||||
|
wire dut_overlap_gate = dut.output_in_overlap;
|
||||||
|
|
||||||
|
// State constants (mirror matched_filter_multi_segment localparams)
|
||||||
|
localparam [3:0] ST_IDLE = 4'd0;
|
||||||
|
localparam [3:0] ST_COLLECT_DATA = 4'd1;
|
||||||
|
localparam [3:0] ST_ZERO_PAD = 4'd2;
|
||||||
|
localparam [3:0] ST_WAIT_REF = 4'd3;
|
||||||
|
localparam [3:0] ST_PROCESSING = 4'd4;
|
||||||
|
localparam [3:0] ST_WAIT_FFT = 4'd5;
|
||||||
|
localparam [3:0] ST_OUTPUT = 4'd6;
|
||||||
|
localparam [3:0] ST_NEXT_SEG = 4'd7;
|
||||||
|
localparam [3:0] ST_OVERLAP_COPY = 4'd8;
|
||||||
|
localparam [3:0] ST_WAIT_LISTEN = 4'd9;
|
||||||
|
|
||||||
|
// ── Helper tasks ─────────────────────────────────────────
|
||||||
|
task do_reset;
|
||||||
|
begin
|
||||||
|
reset_n = 0;
|
||||||
|
mc_new_chirp = 0;
|
||||||
|
mc_new_elevation = 0;
|
||||||
|
mc_new_azimuth = 0;
|
||||||
|
ddc_valid = 0;
|
||||||
|
ddc_i = 0;
|
||||||
|
ddc_q = 0;
|
||||||
|
use_long_chirp = 0;
|
||||||
|
#100;
|
||||||
|
reset_n = 1;
|
||||||
|
@(posedge clk);
|
||||||
|
@(posedge clk); // Let mc_new_chirp_prev settle to 0
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
task wait_n;
|
||||||
|
input integer n;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
for (k = 0; k < n; k = k + 1) @(posedge clk);
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Provide N ddc_valid pulses (continuous, every clock)
|
||||||
|
task provide_samples;
|
||||||
|
input integer n;
|
||||||
|
integer k;
|
||||||
|
begin
|
||||||
|
for (k = 0; k < n; k = k + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_i <= k[17:0];
|
||||||
|
ddc_q <= ~k[17:0];
|
||||||
|
ddc_valid <= 1;
|
||||||
|
end
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 0;
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// Wait for DUT to reach a specific state (with timeout)
|
||||||
|
task wait_for_state;
|
||||||
|
input [3:0] target;
|
||||||
|
input integer timeout_clks;
|
||||||
|
integer t;
|
||||||
|
begin
|
||||||
|
for (t = 0; t < timeout_clks; t = t + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
if (dut_state == target) t = timeout_clks + 1; // break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
endtask
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// MAIN TEST SEQUENCE
|
||||||
|
// ══════════════════════════════════════════════════════════
|
||||||
|
// Counters for overlap trim verification
|
||||||
|
integer seg0_valid_count;
|
||||||
|
integer seg1_valid_count;
|
||||||
|
reg seg0_counting, seg1_counting;
|
||||||
|
reg bin127_suppressed, bin128_passed;
|
||||||
|
|
||||||
|
initial begin
|
||||||
|
$dumpfile("tb_p0_mf_adversarial.vcd");
|
||||||
|
$dumpvars(0, tb_p0_mf_adversarial);
|
||||||
|
|
||||||
|
seg0_valid_count = 0;
|
||||||
|
seg1_valid_count = 0;
|
||||||
|
seg0_counting = 0;
|
||||||
|
seg1_counting = 0;
|
||||||
|
bin127_suppressed = 0;
|
||||||
|
bin128_passed = 0;
|
||||||
|
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP A: TOGGLE DETECTION (Fix #2)
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP A: Toggle Detection (Fix #2) ===");
|
||||||
|
|
||||||
|
// A1: Rising edge (0→1) generates chirp_start_pulse
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_chirp_pulse == 0, "A1 pre: no pulse before toggle");
|
||||||
|
#1; mc_new_chirp = 1; // 0→1
|
||||||
|
@(posedge clk); // pulse should fire (combinational on new vs prev)
|
||||||
|
check(dut_chirp_pulse == 1, "A1: rising edge (0->1) generates pulse");
|
||||||
|
|
||||||
|
// Pulse must be 1 cycle wide
|
||||||
|
@(posedge clk); // mc_new_chirp_prev updates to 1
|
||||||
|
check(dut_chirp_pulse == 0, "A1: pulse is single-cycle (gone on next clock)");
|
||||||
|
|
||||||
|
// Let state machine settle (it entered ST_WAIT_LISTEN)
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// A2: Falling edge (1→0) generates pulse — THIS IS THE FIX
|
||||||
|
#1; mc_new_chirp = 1;
|
||||||
|
@(posedge clk); // prev catches up to 1
|
||||||
|
@(posedge clk); // prev = 1, mc_new_chirp = 1, XOR = 0
|
||||||
|
check(dut_chirp_pulse == 0, "A2 pre: no pulse when stable high");
|
||||||
|
|
||||||
|
#1; mc_new_chirp = 0; // 1→0
|
||||||
|
@(posedge clk); // XOR: 0 ^ 1 = 1
|
||||||
|
check(dut_chirp_pulse == 1, "A2: falling edge (1->0) generates pulse (FIX!)");
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_chirp_pulse == 0, "A2: pulse ends after 1 cycle");
|
||||||
|
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// A3: Stable low — no spurious pulses over 50 clocks
|
||||||
|
begin : stable_low_test
|
||||||
|
reg any_pulse;
|
||||||
|
any_pulse = 0;
|
||||||
|
for (i = 0; i < 50; i = i + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
if (dut_chirp_pulse) any_pulse = 1;
|
||||||
|
end
|
||||||
|
check(!any_pulse, "A3: stable low for 50 clocks — no spurious pulse");
|
||||||
|
end
|
||||||
|
|
||||||
|
// A4: Elevation and azimuth toggles also detected
|
||||||
|
#1; mc_new_elevation = 1; // 0→1
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_elev_pulse == 1, "A4a: elevation toggle 0->1 detected");
|
||||||
|
@(posedge clk);
|
||||||
|
#1; mc_new_elevation = 0; // 1→0
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_elev_pulse == 1, "A4b: elevation toggle 1->0 detected");
|
||||||
|
|
||||||
|
#1; mc_new_azimuth = 1;
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_azim_pulse == 1, "A4c: azimuth toggle 0->1 detected");
|
||||||
|
@(posedge clk);
|
||||||
|
#1; mc_new_azimuth = 0;
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_azim_pulse == 1, "A4d: azimuth toggle 1->0 detected");
|
||||||
|
|
||||||
|
// A5: REGRESSION — verify OLD behavior would have failed
|
||||||
|
// Old code: chirp_start_pulse = mc_new_chirp && !mc_new_chirp_prev
|
||||||
|
// This is a rising-edge detector. On 1→0: 0 && !1 = 0 (missed!)
|
||||||
|
// The NEW XOR code: 0 ^ 1 = 1 (detected!)
|
||||||
|
// We already proved this works in A2. Document the regression:
|
||||||
|
$display(" [INFO] A5 REGRESSION: old AND+NOT code produced 0 for 1->0 transition");
|
||||||
|
$display(" [INFO] old: mc_new_chirp(0) && !mc_new_chirp_prev(1) = 0 && 0 = 0 MISSED");
|
||||||
|
$display(" [INFO] new: mc_new_chirp(0) ^ mc_new_chirp_prev(1) = 0 ^ 1 = 1 DETECTED");
|
||||||
|
check(1, "A5: REGRESSION documented — falling edge was missed by old code");
|
||||||
|
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP B: LISTEN DELAY (Fix #3)
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP B: Listen Delay (Fix #3) ===");
|
||||||
|
|
||||||
|
// Use SHORT chirp: listen_delay_target = TB_SHORT_CHIRP = 10
|
||||||
|
#1; use_long_chirp = 0;
|
||||||
|
|
||||||
|
// B1: Chirp start → enters ST_WAIT_LISTEN (not ST_COLLECT_DATA)
|
||||||
|
mc_new_chirp = 1; // toggle 0→1
|
||||||
|
@(posedge clk); // pulse fires, state machine acts
|
||||||
|
@(posedge clk); // non-blocking assignment settles
|
||||||
|
check(dut_state == ST_WAIT_LISTEN, "B1: enters ST_WAIT_LISTEN (not COLLECT_DATA)");
|
||||||
|
check(dut_listen_target == TB_SHORT_CHIRP,
|
||||||
|
"B1: listen_delay_target = SHORT_CHIRP_SAMPLES");
|
||||||
|
|
||||||
|
// B2: Counter increments only on ddc_valid
|
||||||
|
// Provide 5 valid pulses, then 5 clocks without valid, then 5 more valid
|
||||||
|
for (i = 0; i < 5; i = i + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 1;
|
||||||
|
ddc_i <= i[17:0];
|
||||||
|
ddc_q <= 0;
|
||||||
|
end
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 0;
|
||||||
|
|
||||||
|
// Counter should be 5 after 5 valid pulses
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_listen_count == 5, "B2a: counter = 5 after 5 valid pulses");
|
||||||
|
check(dut_state == ST_WAIT_LISTEN, "B2a: still in ST_WAIT_LISTEN");
|
||||||
|
|
||||||
|
// B3: 5 clocks with no valid — counter must NOT advance
|
||||||
|
wait_n(5);
|
||||||
|
check(dut_listen_count == 5, "B3: counter stays 5 during ddc_valid gaps");
|
||||||
|
check(dut_state == ST_WAIT_LISTEN, "B3: still in ST_WAIT_LISTEN");
|
||||||
|
|
||||||
|
// B4: Provide remaining pulses to hit boundary
|
||||||
|
// Need 5 more valid pulses (total 10 = TB_SHORT_CHIRP)
|
||||||
|
// Counter transitions at >= target-1 = 9, so pulse 10 triggers
|
||||||
|
for (i = 0; i < 4; i = i + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 1;
|
||||||
|
ddc_i <= (i + 5);
|
||||||
|
ddc_q <= 0;
|
||||||
|
end
|
||||||
|
// After 4 more: count = 9 = target-1 → transition happens on THIS valid
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 1; // 10th pulse
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 0;
|
||||||
|
@(posedge clk); // Let non-blocking assignments settle
|
||||||
|
|
||||||
|
check(dut_state == ST_COLLECT_DATA,
|
||||||
|
"B4: transitions to ST_COLLECT_DATA after exact delay count");
|
||||||
|
|
||||||
|
// B5: First sample collected is the one AFTER the delay
|
||||||
|
// The module is now in ST_COLLECT_DATA. Provide a sample and verify
|
||||||
|
// it gets written to the buffer (buffer_write_ptr should advance)
|
||||||
|
begin : first_sample_check
|
||||||
|
reg [10:0] ptr_before;
|
||||||
|
ptr_before = dut.buffer_write_ptr;
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 1;
|
||||||
|
ddc_i <= 18'h1FACE;
|
||||||
|
ddc_q <= 18'h1BEEF;
|
||||||
|
@(posedge clk);
|
||||||
|
ddc_valid <= 0;
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut.buffer_write_ptr == ptr_before + 1,
|
||||||
|
"B5: first echo sample collected (write_ptr advanced)");
|
||||||
|
end
|
||||||
|
|
||||||
|
do_reset;
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
// GROUP C: OVERLAP-SAVE OUTPUT TRIM (Fix #4)
|
||||||
|
// ──────────────────────────────────────────────────────
|
||||||
|
$display("\n=== GROUP C: Overlap-Save Output Trim (Fix #4) ===");
|
||||||
|
|
||||||
|
// Use LONG chirp with 2+ segments for overlap trim testing
|
||||||
|
#1; use_long_chirp = 1;
|
||||||
|
seg0_valid_count = 0;
|
||||||
|
seg1_valid_count = 0;
|
||||||
|
|
||||||
|
// C-SETUP: Trigger chirp, pass through listen delay, process 2 segments
|
||||||
|
mc_new_chirp = 1; // toggle 0→1
|
||||||
|
@(posedge clk);
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_state == ST_WAIT_LISTEN, "C-setup: entered ST_WAIT_LISTEN");
|
||||||
|
check(dut_listen_target == TB_LONG_CHIRP,
|
||||||
|
"C-setup: listen target = LONG_CHIRP_SAMPLES");
|
||||||
|
|
||||||
|
// Pass through listen delay: provide TB_LONG_CHIRP (2000) ddc_valid pulses
|
||||||
|
$display(" [INFO] Providing %0d listen-delay samples...", TB_LONG_CHIRP);
|
||||||
|
provide_samples(TB_LONG_CHIRP);
|
||||||
|
|
||||||
|
// Should now be in ST_COLLECT_DATA
|
||||||
|
@(posedge clk);
|
||||||
|
check(dut_state == ST_COLLECT_DATA,
|
||||||
|
"C-setup: in ST_COLLECT_DATA after listen delay");
|
||||||
|
|
||||||
|
// ── SEGMENT 0: Collect 1024 samples ──
|
||||||
|
$display(" [INFO] Providing 1024 echo samples for segment 0...");
|
||||||
|
provide_samples(TB_BUF_SIZE);
|
||||||
|
|
||||||
|
// Should transition through WAIT_REF → PROCESSING → WAIT_FFT
|
||||||
|
// mem_ready is always 1, so WAIT_REF passes immediately
|
||||||
|
wait_for_state(ST_WAIT_FFT, 2000);
|
||||||
|
check(dut_state == ST_WAIT_FFT, "C-setup: seg0 reached ST_WAIT_FFT");
|
||||||
|
check(dut_segment == 0, "C-setup: processing segment 0");
|
||||||
|
|
||||||
|
// During ST_WAIT_FFT, the stub chain outputs 1024 fft_pc_valid pulses.
|
||||||
|
// Count pc_valid_w (the gated output) for segment 0.
|
||||||
|
seg0_counting = 1;
|
||||||
|
wait_for_state(ST_OUTPUT, 2000);
|
||||||
|
seg0_counting = 0;
|
||||||
|
|
||||||
|
// C1: Segment 0 — ALL output bins should pass (no trim)
|
||||||
|
check(seg0_valid_count == TB_BUF_SIZE,
|
||||||
|
"C1: segment 0 — all 1024 output bins pass (no trim)");
|
||||||
|
|
||||||
|
// Let state machine proceed to next segment
|
||||||
|
wait_for_state(ST_COLLECT_DATA, 500);
|
||||||
|
check(dut_segment == 1, "C-setup: advanced to segment 1");
|
||||||
|
|
||||||
|
// ── SEGMENT 1: Collect 896 samples (buffer starts at 128 from overlap) ──
|
||||||
|
$display(" [INFO] Providing %0d echo samples for segment 1...", TB_SEG_ADVANCE);
|
||||||
|
provide_samples(TB_SEG_ADVANCE);
|
||||||
|
|
||||||
|
// Wait for seg 1 processing
|
||||||
|
wait_for_state(ST_WAIT_FFT, 2000);
|
||||||
|
check(dut_state == ST_WAIT_FFT, "C-setup: seg1 reached ST_WAIT_FFT");
|
||||||
|
|
||||||
|
// Count pc_valid_w during segment 1 output
|
||||||
|
seg1_counting = 1;
|
||||||
|
bin127_suppressed = 0;
|
||||||
|
bin128_passed = 0;
|
||||||
|
|
||||||
|
// Monitor specific boundary bins during chain output
|
||||||
|
begin : seg1_output_monitor
|
||||||
|
integer wait_count;
|
||||||
|
for (wait_count = 0; wait_count < 2000; wait_count = wait_count + 1) begin
|
||||||
|
@(posedge clk);
|
||||||
|
|
||||||
|
// Check boundary: bin 127 should be suppressed
|
||||||
|
if (dut_out_bin_count == 127 && dut.fft_pc_valid) begin
|
||||||
|
if (pc_valid_w == 0) bin127_suppressed = 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Check boundary: bin 128 should pass
|
||||||
|
if (dut_out_bin_count == 128 && dut.fft_pc_valid) begin
|
||||||
|
if (pc_valid_w == 1) bin128_passed = 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
if (dut_state == ST_OUTPUT) begin
|
||||||
|
wait_count = 9999; // break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
seg1_counting = 0;
|
||||||
|
|
||||||
|
// C2: Segment 1 — first 128 bins suppressed, 896 pass
|
||||||
|
check(seg1_valid_count == TB_SEG_ADVANCE,
|
||||||
|
"C2: segment 1 — exactly 896 output bins pass (128 trimmed)");
|
||||||
|
|
||||||
|
// C3: Boundary bin accuracy
|
||||||
|
check(bin127_suppressed, "C3a: bin 127 suppressed (overlap artifact)");
|
||||||
|
check(bin128_passed, "C3b: bin 128 passes (first valid bin)");
|
||||||
|
|
||||||
|
// C4: Overlap gate signal logic
|
||||||
|
// For segment != 0, output_in_overlap should be true when bin_count < 128
|
||||||
|
check(dut_segment == 1, "C4 pre: still on segment 1");
|
||||||
|
// (Gate was already verified implicitly by C2/C3 counts)
|
||||||
|
check(1, "C4: overlap gate correctly suppresses bins [0..127] on seg 1+");
|
||||||
|
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
// SUMMARY
|
||||||
|
// ══════════════════════════════════════════════════════
|
||||||
|
$display("\n============================================");
|
||||||
|
$display(" P0 Fixes #2/#3/#4: MF Adversarial Tests");
|
||||||
|
$display("============================================");
|
||||||
|
$display(" PASSED: %0d", pass_count);
|
||||||
|
$display(" FAILED: %0d", fail_count);
|
||||||
|
$display("============================================");
|
||||||
|
|
||||||
|
if (fail_count > 0)
|
||||||
|
$display("RESULT: FAIL");
|
||||||
|
else
|
||||||
|
$display("RESULT: PASS");
|
||||||
|
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
// ── Continuous counters for overlap trim verification ────
|
||||||
|
always @(posedge clk) begin
|
||||||
|
if (seg0_counting && pc_valid_w)
|
||||||
|
seg0_valid_count <= seg0_valid_count + 1;
|
||||||
|
if (seg1_counting && pc_valid_w)
|
||||||
|
seg1_valid_count <= seg1_valid_count + 1;
|
||||||
|
end
|
||||||
|
|
||||||
|
// Timeout watchdog (generous for 2000-sample listen delay + 2 segments)
|
||||||
|
initial begin
|
||||||
|
#5000000;
|
||||||
|
$display("[FAIL] TIMEOUT: simulation exceeded 5ms");
|
||||||
|
$finish;
|
||||||
|
end
|
||||||
|
|
||||||
|
endmodule
|
||||||
@@ -497,6 +497,7 @@ def count_concat_bits(concat_expr: str, port_widths: dict[str, int]) -> ConcatWi
|
|||||||
# Unknown width — flag it
|
# Unknown width — flag it
|
||||||
fragments.append((part, -1))
|
fragments.append((part, -1))
|
||||||
total = -1 # Can't compute
|
total = -1 # Can't compute
|
||||||
|
break
|
||||||
|
|
||||||
return ConcatWidth(
|
return ConcatWidth(
|
||||||
total_bits=total,
|
total_bits=total,
|
||||||
|
|||||||
@@ -49,8 +49,8 @@ sys.path.insert(0, str(cp.GUI_DIR))
|
|||||||
# Helpers
|
# Helpers
|
||||||
# ===================================================================
|
# ===================================================================
|
||||||
|
|
||||||
IVERILOG = os.environ.get("IVERILOG", "/opt/homebrew/bin/iverilog")
|
IVERILOG = os.environ.get("IVERILOG", "iverilog")
|
||||||
VVP = os.environ.get("VVP", "/opt/homebrew/bin/vvp")
|
VVP = os.environ.get("VVP", "vvp")
|
||||||
CXX = os.environ.get("CXX", "c++")
|
CXX = os.environ.get("CXX", "c++")
|
||||||
|
|
||||||
# Check tool availability for conditional skipping
|
# Check tool availability for conditional skipping
|
||||||
@@ -61,6 +61,20 @@ _has_cxx = subprocess.run(
|
|||||||
[CXX, "--version"], capture_output=True
|
[CXX, "--version"], capture_output=True
|
||||||
).returncode == 0
|
).returncode == 0
|
||||||
|
|
||||||
|
# In CI, missing tools must be a hard failure — never silently skip.
|
||||||
|
_in_ci = os.environ.get("GITHUB_ACTIONS") == "true"
|
||||||
|
if _in_ci:
|
||||||
|
if not _has_iverilog:
|
||||||
|
raise RuntimeError(
|
||||||
|
"iverilog is required in CI but was not found. "
|
||||||
|
"Ensure 'apt-get install iverilog' ran and IVERILOG/VVP are on PATH."
|
||||||
|
)
|
||||||
|
if not _has_cxx:
|
||||||
|
raise RuntimeError(
|
||||||
|
"C++ compiler is required in CI but was not found. "
|
||||||
|
"Ensure build-essential is installed."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _parse_hex_results(text: str) -> list[dict[str, str]]:
|
def _parse_hex_results(text: str) -> list[dict[str, str]]:
|
||||||
"""Parse space-separated hex lines from TB output files."""
|
"""Parse space-separated hex lines from TB output files."""
|
||||||
|
|||||||
Reference in New Issue
Block a user