Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7edbd2d3d0 | |||
| 658752abb7 |
@@ -18,7 +18,7 @@ ADAR1000_AGC::ADAR1000_AGC()
|
||||
, min_gain(0)
|
||||
, max_gain(127)
|
||||
, holdoff_frames(4)
|
||||
, enabled(true)
|
||||
, enabled(false)
|
||||
, holdoff_counter(0)
|
||||
, last_saturated(false)
|
||||
, saturation_event_count(0)
|
||||
|
||||
@@ -2180,9 +2180,24 @@ int main(void)
|
||||
|
||||
runRadarPulseSequence();
|
||||
|
||||
/* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13),
|
||||
* adjust ADAR1000 VGA common gain once per radar frame (~258 ms).
|
||||
* Only run when AGC is enabled — otherwise leave VGA gains untouched. */
|
||||
/* [AGC] Outer-loop AGC: sync enable from FPGA via DIG_6 (PD14),
|
||||
* then read saturation flag (DIG_5 / PD13) and adjust ADAR1000 VGA
|
||||
* common gain once per radar frame (~258 ms).
|
||||
* FPGA register host_agc_enable is the single source of truth —
|
||||
* DIG_6 propagates it to MCU every frame.
|
||||
* 2-frame confirmation debounce: only change outerAgc.enabled when
|
||||
* two consecutive frames read the same DIG_6 value. Prevents a
|
||||
* single-sample glitch from causing a spurious AGC state transition.
|
||||
* Added latency: 1 extra frame (~258 ms), acceptable for control plane. */
|
||||
{
|
||||
bool dig6_now = (HAL_GPIO_ReadPin(FPGA_DIG6_GPIO_Port,
|
||||
FPGA_DIG6_Pin) == GPIO_PIN_SET);
|
||||
static bool dig6_prev = false; // matches boot default (AGC off)
|
||||
if (dig6_now == dig6_prev) {
|
||||
outerAgc.enabled = dig6_now;
|
||||
}
|
||||
dig6_prev = dig6_now;
|
||||
}
|
||||
if (outerAgc.enabled) {
|
||||
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
||||
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
||||
|
||||
@@ -50,7 +50,7 @@ static void test_defaults()
|
||||
assert(agc.min_gain == 0);
|
||||
assert(agc.max_gain == 127);
|
||||
assert(agc.holdoff_frames == 4);
|
||||
assert(agc.enabled == true);
|
||||
assert(agc.enabled == false); // disabled by default — FPGA DIG_6 is source of truth
|
||||
assert(agc.holdoff_counter == 0);
|
||||
assert(agc.last_saturated == false);
|
||||
assert(agc.saturation_event_count == 0);
|
||||
@@ -67,6 +67,7 @@ static void test_defaults()
|
||||
static void test_saturation_reduces_gain()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
uint8_t initial = agc.agc_base_gain; // 30
|
||||
|
||||
agc.update(true); // saturation
|
||||
@@ -82,6 +83,7 @@ static void test_saturation_reduces_gain()
|
||||
static void test_holdoff_prevents_early_gain_up()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.update(true); // saturate once -> gain = 26
|
||||
uint8_t after_sat = agc.agc_base_gain;
|
||||
|
||||
@@ -101,6 +103,7 @@ static void test_holdoff_prevents_early_gain_up()
|
||||
static void test_recovery_after_holdoff()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.update(true); // saturate -> gain = 26
|
||||
uint8_t after_sat = agc.agc_base_gain;
|
||||
|
||||
@@ -119,6 +122,7 @@ static void test_recovery_after_holdoff()
|
||||
static void test_min_gain_clamp()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.min_gain = 10;
|
||||
agc.agc_base_gain = 12;
|
||||
agc.gain_step_down = 4;
|
||||
@@ -136,6 +140,7 @@ static void test_min_gain_clamp()
|
||||
static void test_max_gain_clamp()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.max_gain = 32;
|
||||
agc.agc_base_gain = 31;
|
||||
agc.gain_step_up = 2;
|
||||
@@ -226,6 +231,7 @@ static void test_apply_gain_spi()
|
||||
static void test_reset_preserves_config()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.agc_base_gain = 42;
|
||||
agc.gain_step_down = 8;
|
||||
agc.cal_offset[3] = -5;
|
||||
@@ -255,6 +261,7 @@ static void test_reset_preserves_config()
|
||||
static void test_saturation_counter()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
agc.update(true);
|
||||
@@ -274,6 +281,7 @@ static void test_saturation_counter()
|
||||
static void test_mixed_sequence()
|
||||
{
|
||||
ADAR1000_AGC agc;
|
||||
agc.enabled = true; // default is OFF; enable for this test
|
||||
agc.agc_base_gain = 30;
|
||||
agc.gain_step_down = 4;
|
||||
agc.gain_step_up = 1;
|
||||
|
||||
@@ -137,145 +137,6 @@ module cdc_adc_to_processing #(
|
||||
|
||||
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
|
||||
// Uses synchronous reset on sync chain to avoid metastability on reset
|
||||
|
||||
@@ -224,7 +224,7 @@ set_property IOSTANDARD LVCMOS33 [get_ports {stm32_mixers_enable}]
|
||||
|
||||
# DIG_5 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs
|
||||
# DIG_5: AGC saturation flag (PD13 on STM32)
|
||||
# DIG_6: reserved (PD14)
|
||||
# DIG_6: AGC enable flag (PD14) — mirrors FPGA host_agc_enable to STM32
|
||||
# DIG_7: reserved (PD15)
|
||||
set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}]
|
||||
set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}]
|
||||
|
||||
@@ -584,59 +584,41 @@ cic_decimator_4x_enhanced cic_q_inst (
|
||||
assign cic_valid = cic_valid_i & cic_valid_q;
|
||||
|
||||
// ============================================================================
|
||||
// Clock Domain Crossing: 400 MHz CIC output → 100 MHz FIR input
|
||||
// ============================================================================
|
||||
// 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.
|
||||
// Enhanced FIR Filters with FIXED valid signal handling
|
||||
// NOTE: Wire declarations moved BEFORE CDC instances to fix forward-reference
|
||||
// error in Icarus Verilog (was originally after CDC instantiation)
|
||||
// ============================================================================
|
||||
wire fir_in_valid_i, fir_in_valid_q;
|
||||
wire fir_valid_i, fir_valid_q;
|
||||
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;
|
||||
|
||||
// I-channel CDC: async FIFO, 400 MHz write → 100 MHz read
|
||||
cdc_async_fifo #(
|
||||
.WIDTH(18),
|
||||
.DEPTH(8),
|
||||
.ADDR_BITS(3)
|
||||
) CDC_FIR_i (
|
||||
.wr_clk(clk_400m),
|
||||
.wr_reset_n(reset_n_400m),
|
||||
.wr_data(cic_i_out),
|
||||
.wr_en(cic_valid_i),
|
||||
.wr_full(), // At 1:1 data rate, overflow should not occur
|
||||
|
||||
.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 #(
|
||||
.WIDTH(18),
|
||||
.STAGES(3)
|
||||
)CDC_FIR_i(
|
||||
.src_clk(clk_400m),
|
||||
.dst_clk(clk_100m),
|
||||
.src_reset_n(reset_n_400m),
|
||||
.dst_reset_n(reset_n),
|
||||
.src_data(cic_i_out),
|
||||
.src_valid(cic_valid_i),
|
||||
.dst_data(fir_d_in_i),
|
||||
.dst_valid(fir_in_valid_i)
|
||||
);
|
||||
|
||||
// Q-channel CDC: async FIFO, 400 MHz write → 100 MHz read
|
||||
cdc_async_fifo #(
|
||||
.WIDTH(18),
|
||||
.DEPTH(8),
|
||||
.ADDR_BITS(3)
|
||||
) CDC_FIR_q (
|
||||
.wr_clk(clk_400m),
|
||||
.wr_reset_n(reset_n_400m),
|
||||
.wr_data(cic_q_out),
|
||||
.wr_en(cic_valid_q),
|
||||
.wr_full(),
|
||||
|
||||
.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)
|
||||
cdc_adc_to_processing #(
|
||||
.WIDTH(18),
|
||||
.STAGES(3)
|
||||
)CDC_FIR_q(
|
||||
.src_clk(clk_400m),
|
||||
.dst_clk(clk_100m),
|
||||
.src_reset_n(reset_n_400m),
|
||||
.dst_reset_n(reset_n),
|
||||
.src_data(cic_q_out),
|
||||
.src_valid(cic_valid_q),
|
||||
.dst_data(fir_d_in_q),
|
||||
.dst_valid(fir_in_valid_q)
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -531,23 +531,6 @@ xfft_16 fft_inst (
|
||||
// Status Outputs
|
||||
// ==============================================
|
||||
assign processing_active = (state != S_IDLE);
|
||||
|
||||
// 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);
|
||||
assign frame_complete = (state == S_IDLE && frame_buffer_full == 0);
|
||||
|
||||
endmodule
|
||||
|
||||
@@ -77,7 +77,6 @@ reg signed [15:0] buf_rdata_i, buf_rdata_q;
|
||||
// State machine
|
||||
reg [3:0] state;
|
||||
localparam ST_IDLE = 0;
|
||||
localparam ST_WAIT_LISTEN = 9; // Wait for TX chirp to end before collecting
|
||||
localparam ST_COLLECT_DATA = 1;
|
||||
localparam ST_ZERO_PAD = 2;
|
||||
localparam ST_WAIT_REF = 3;
|
||||
@@ -99,22 +98,11 @@ reg signed [15:0] overlap_cache_i [0:OVERLAP_SAMPLES-1];
|
||||
reg signed [15:0] overlap_cache_q [0:OVERLAP_SAMPLES-1];
|
||||
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
|
||||
// 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;
|
||||
wire chirp_start_pulse = mc_new_chirp ^ mc_new_chirp_prev;
|
||||
wire elevation_change_pulse = mc_new_elevation ^ mc_new_elevation_prev;
|
||||
wire azimuth_change_pulse = mc_new_azimuth ^ mc_new_azimuth_prev;
|
||||
wire chirp_start_pulse = mc_new_chirp && !mc_new_chirp_prev;
|
||||
wire elevation_change_pulse = mc_new_elevation && !mc_new_elevation_prev;
|
||||
wire azimuth_change_pulse = mc_new_azimuth && !mc_new_azimuth_prev;
|
||||
|
||||
// Processing chain signals
|
||||
wire [15:0] fft_pc_i, fft_pc_q;
|
||||
@@ -196,8 +184,6 @@ always @(posedge clk or negedge reset_n) begin
|
||||
buf_wdata_q <= 0;
|
||||
buf_raddr <= 0;
|
||||
overlap_copy_count <= 0;
|
||||
listen_delay_count <= 0;
|
||||
listen_delay_target <= 0;
|
||||
end else begin
|
||||
pc_valid <= 0;
|
||||
mem_request <= 0;
|
||||
@@ -219,45 +205,19 @@ always @(posedge clk or negedge reset_n) begin
|
||||
|
||||
// Wait for chirp start from microcontroller
|
||||
if (chirp_start_pulse) begin
|
||||
state <= ST_COLLECT_DATA;
|
||||
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
|
||||
$display("[MULTI_SEG_FIXED] Chirp start detected, waiting for listen window (%0d samples)",
|
||||
use_long_chirp ? LONG_CHIRP_SAMPLES : SHORT_CHIRP_SAMPLES);
|
||||
$display("[MULTI_SEG_FIXED] Starting %s chirp, segments: %d",
|
||||
use_long_chirp ? "LONG" : "SHORT",
|
||||
use_long_chirp ? LONG_SEGMENTS : SHORT_SEGMENTS);
|
||||
$display("[MULTI_SEG_FIXED] Overlap: %d samples, Advance: %d samples",
|
||||
OVERLAP_SAMPLES, SEGMENT_ADVANCE);
|
||||
`endif
|
||||
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
|
||||
// Collect samples for current segment with overlap-save
|
||||
if (ddc_valid && buffer_write_ptr < BUFFER_SIZE) begin
|
||||
@@ -574,36 +534,9 @@ always @(posedge clk or negedge reset_n) begin
|
||||
end
|
||||
`endif
|
||||
|
||||
// ========== 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
|
||||
|
||||
// ========== OUTPUT CONNECTIONS ==========
|
||||
assign pc_i_w = fft_pc_i;
|
||||
assign pc_q_w = fft_pc_q;
|
||||
assign pc_valid_w = fft_pc_valid & ~output_in_overlap;
|
||||
assign pc_valid_w = fft_pc_valid;
|
||||
|
||||
endmodule
|
||||
@@ -130,7 +130,7 @@ module radar_system_top (
|
||||
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
|
||||
// Used by STM32 outer AGC loop to read saturation state without USB polling.
|
||||
output wire gpio_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected)
|
||||
output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low)
|
||||
output wire gpio_dig6, // DIG_6 (G12→PD14): AGC enable flag (mirrors host_agc_enable)
|
||||
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
||||
);
|
||||
|
||||
@@ -1037,9 +1037,11 @@ assign system_status = status_reg;
|
||||
// ============================================================================
|
||||
// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0.
|
||||
// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain.
|
||||
// DIG_6, DIG_7: Reserved (tied low for future use).
|
||||
// DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC
|
||||
// tracks the FPGA register as single source of truth.
|
||||
// DIG_7: Reserved (tied low for future use).
|
||||
assign gpio_dig5 = (rx_agc_saturation_count != 8'd0);
|
||||
assign gpio_dig6 = 1'b0;
|
||||
assign gpio_dig6 = host_agc_enable;
|
||||
assign gpio_dig7 = 1'b0;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -526,25 +526,6 @@ run_test "Radar Mode Controller" \
|
||||
|
||||
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
|
||||
# ===========================================================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,558 +0,0 @@
|
||||
`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
|
||||
@@ -1,361 +0,0 @@
|
||||
`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
|
||||
@@ -1,602 +0,0 @@
|
||||
`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
|
||||
@@ -27,6 +27,7 @@ layers agree (because both could be wrong).
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import subprocess
|
||||
import tempfile
|
||||
@@ -369,6 +370,188 @@ class TestTier1ResetDefaults:
|
||||
)
|
||||
|
||||
|
||||
class TestTier1AgcCrossLayerInvariant:
|
||||
"""
|
||||
Verify AGC enable/disable is consistent across FPGA, MCU, and GUI layers.
|
||||
|
||||
System-level invariant: the FPGA register host_agc_enable is the single
|
||||
source of truth for AGC state. It propagates to MCU via DIG_6 GPIO and
|
||||
to GUI via status word 4 bit[11]. At boot, all layers must agree AGC=OFF.
|
||||
At runtime, the MCU must read DIG_6 every frame to sync its outer-loop AGC.
|
||||
"""
|
||||
|
||||
def test_fpga_dig6_drives_agc_enable(self):
|
||||
"""FPGA must drive gpio_dig6 from host_agc_enable, NOT tied low."""
|
||||
rtl = (cp.FPGA_DIR / "radar_system_top.v").read_text()
|
||||
# Must find: assign gpio_dig6 = host_agc_enable;
|
||||
assert re.search(
|
||||
r'assign\s+gpio_dig6\s*=\s*host_agc_enable\s*;', rtl
|
||||
), "gpio_dig6 must be driven by host_agc_enable (not tied low)"
|
||||
# Must NOT have the old tied-low pattern
|
||||
assert not re.search(
|
||||
r"assign\s+gpio_dig6\s*=\s*1'b0\s*;", rtl
|
||||
), "gpio_dig6 must NOT be tied low — it carries AGC enable"
|
||||
|
||||
def test_fpga_agc_enable_boot_default_off(self):
|
||||
"""FPGA host_agc_enable must reset to 0 (AGC off at boot)."""
|
||||
v_defaults = cp.parse_verilog_reset_defaults()
|
||||
assert "host_agc_enable" in v_defaults, (
|
||||
"host_agc_enable not found in reset block"
|
||||
)
|
||||
assert v_defaults["host_agc_enable"] == 0, (
|
||||
f"host_agc_enable reset default is {v_defaults['host_agc_enable']}, "
|
||||
"expected 0 (AGC off at boot)"
|
||||
)
|
||||
|
||||
def test_mcu_agc_constructor_default_off(self):
|
||||
"""MCU ADAR1000_AGC constructor must default enabled=false."""
|
||||
agc_cpp = (cp.MCU_LIB_DIR / "ADAR1000_AGC.cpp").read_text()
|
||||
# The constructor initializer list must have enabled(false)
|
||||
assert re.search(
|
||||
r'enabled\s*\(\s*false\s*\)', agc_cpp
|
||||
), "ADAR1000_AGC constructor must initialize enabled(false)"
|
||||
assert not re.search(
|
||||
r'enabled\s*\(\s*true\s*\)', agc_cpp
|
||||
), "ADAR1000_AGC constructor must NOT initialize enabled(true)"
|
||||
|
||||
def test_mcu_reads_dig6_before_agc_gate(self):
|
||||
"""MCU main loop must read DIG_6 GPIO to sync outerAgc.enabled."""
|
||||
main_cpp = (cp.MCU_CODE_DIR / "main.cpp").read_text()
|
||||
# DIG_6 must be read via HAL_GPIO_ReadPin
|
||||
assert re.search(
|
||||
r'HAL_GPIO_ReadPin\s*\(\s*FPGA_DIG6', main_cpp,
|
||||
), "main.cpp must read DIG_6 GPIO via HAL_GPIO_ReadPin"
|
||||
# outerAgc.enabled must be assigned from the DIG_6 reading
|
||||
# (may be indirect via debounce variable like dig6_now)
|
||||
assert re.search(
|
||||
r'outerAgc\.enabled\s*=', main_cpp,
|
||||
), "main.cpp must assign outerAgc.enabled from DIG_6 state"
|
||||
|
||||
def test_boot_invariant_all_layers_agc_off(self):
|
||||
"""
|
||||
At boot, all three layers must agree: AGC is OFF.
|
||||
- FPGA: host_agc_enable resets to 0 -> DIG_6 low
|
||||
- MCU: ADAR1000_AGC.enabled defaults to false
|
||||
- GUI: reads status word 4 bit[11] = 0 -> reports MANUAL
|
||||
"""
|
||||
# FPGA
|
||||
v_defaults = cp.parse_verilog_reset_defaults()
|
||||
assert v_defaults.get("host_agc_enable") == 0
|
||||
|
||||
# MCU
|
||||
agc_cpp = (cp.MCU_LIB_DIR / "ADAR1000_AGC.cpp").read_text()
|
||||
assert re.search(r'enabled\s*\(\s*false\s*\)', agc_cpp)
|
||||
|
||||
# GUI: status word 4 bit[11] is host_agc_enable, which resets to 0.
|
||||
# Verify the GUI parses bit[11] of status word 4 as the AGC flag.
|
||||
gui_py = (cp.GUI_DIR / "radar_protocol.py").read_text()
|
||||
assert re.search(
|
||||
r'words\[4\].*>>\s*11|status_words\[4\].*>>\s*11',
|
||||
gui_py,
|
||||
), "GUI must parse AGC status from words[4] bit[11]"
|
||||
|
||||
def test_status_word4_agc_bit_matches_dig6_source(self):
|
||||
"""
|
||||
Status word 4 bit[11] and DIG_6 must both derive from host_agc_enable.
|
||||
This guarantees the GUI status display can never lie about MCU AGC state.
|
||||
"""
|
||||
rtl = (cp.FPGA_DIR / "radar_system_top.v").read_text()
|
||||
|
||||
# DIG_6 driven by host_agc_enable
|
||||
assert re.search(
|
||||
r'assign\s+gpio_dig6\s*=\s*host_agc_enable\s*;', rtl
|
||||
)
|
||||
|
||||
# Status word 4 must contain host_agc_enable (may be named
|
||||
# status_agc_enable at the USB interface port boundary).
|
||||
# Also verify the top-level wiring connects them.
|
||||
usb_ft2232h = (cp.FPGA_DIR / "usb_data_interface_ft2232h.v").read_text()
|
||||
usb_ft601 = (cp.FPGA_DIR / "usb_data_interface.v").read_text()
|
||||
|
||||
# USB interfaces use the port name status_agc_enable
|
||||
found_in_ft2232h = "status_agc_enable" in usb_ft2232h
|
||||
found_in_ft601 = "status_agc_enable" in usb_ft601
|
||||
|
||||
assert found_in_ft2232h or found_in_ft601, (
|
||||
"status_agc_enable must appear in at least one USB interface's "
|
||||
"status word to guarantee GUI status matches DIG_6"
|
||||
)
|
||||
|
||||
# Verify top-level wiring: status_agc_enable port is connected
|
||||
# to host_agc_enable (same signal that drives DIG_6)
|
||||
assert re.search(
|
||||
r'\.status_agc_enable\s*\(\s*host_agc_enable\s*\)', rtl
|
||||
), (
|
||||
"Top-level must wire .status_agc_enable(host_agc_enable) "
|
||||
"so status word and DIG_6 derive from the same signal"
|
||||
)
|
||||
|
||||
def test_mcu_dig6_debounce_guards_enable_assignment(self):
|
||||
"""
|
||||
MCU must apply a 2-frame confirmation debounce before mutating
|
||||
outerAgc.enabled from DIG_6 reads. A naive assignment straight from
|
||||
the latest GPIO sample would let a single-cycle glitch flip the AGC
|
||||
state for one frame.
|
||||
"""
|
||||
main_cpp = (cp.MCU_CODE_DIR / "main.cpp").read_text()
|
||||
|
||||
# (1) Current-frame DIG_6 sample must be captured in a local variable
|
||||
# so it can be compared against the previous-frame value.
|
||||
now_match = re.search(
|
||||
r'(bool|int|uint8_t)\s+(\w*dig6\w*)\s*=\s*[^;]*?'
|
||||
r'HAL_GPIO_ReadPin\s*\(\s*FPGA_DIG6[^;]*;',
|
||||
main_cpp,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert now_match, (
|
||||
"DIG_6 read must be stored in a local variable (e.g. `dig6_now`) "
|
||||
"so the current sample can be compared against the previous frame"
|
||||
)
|
||||
now_var = now_match.group(2)
|
||||
|
||||
# (2) Previous-frame state must persist across iterations via static
|
||||
# storage, and must default to false (matches FPGA boot: AGC off).
|
||||
prev_match = re.search(
|
||||
r'static\s+(bool|int|uint8_t)\s+(\w*dig6\w*)\s*=\s*(false|0)\s*;',
|
||||
main_cpp,
|
||||
)
|
||||
assert prev_match, (
|
||||
"A static previous-frame variable (e.g. "
|
||||
"`static bool dig6_prev = false;`) must exist, initialized to "
|
||||
"false so the debounce starts in sync with the FPGA boot default"
|
||||
)
|
||||
prev_var = prev_match.group(2)
|
||||
assert prev_var != now_var, (
|
||||
f"Current and previous DIG_6 variables must be distinct "
|
||||
f"(both are '{now_var}')"
|
||||
)
|
||||
|
||||
# (3) outerAgc.enabled assignment must be gated by now == prev.
|
||||
guarded_assign = re.search(
|
||||
rf'if\s*\(\s*{now_var}\s*==\s*{prev_var}\s*\)\s*\{{[^}}]*?'
|
||||
rf'outerAgc\.enabled\s*=\s*{now_var}\s*;',
|
||||
main_cpp,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert guarded_assign, (
|
||||
f"`outerAgc.enabled = {now_var};` must be inside "
|
||||
f"`if ({now_var} == {prev_var}) {{ ... }}` — the confirmation "
|
||||
"guard that absorbs single-sample GPIO glitches. A naive "
|
||||
"assignment without this guard reintroduces the glitch bug."
|
||||
)
|
||||
|
||||
# (4) Previous-frame variable must advance each frame.
|
||||
prev_update = re.search(
|
||||
rf'{prev_var}\s*=\s*{now_var}\s*;',
|
||||
main_cpp,
|
||||
)
|
||||
assert prev_update, (
|
||||
f"`{prev_var} = {now_var};` must run each frame so the "
|
||||
"debounce window slides forward; without it the guard is "
|
||||
"stuck and enable changes never confirm"
|
||||
)
|
||||
|
||||
|
||||
class TestTier1DataPacketLayout:
|
||||
"""Verify data packet byte layout matches between Python and Verilog."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user