Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7742b517b6 |
@@ -18,7 +18,7 @@ ADAR1000_AGC::ADAR1000_AGC()
|
|||||||
, min_gain(0)
|
, min_gain(0)
|
||||||
, max_gain(127)
|
, max_gain(127)
|
||||||
, holdoff_frames(4)
|
, holdoff_frames(4)
|
||||||
, enabled(false)
|
, enabled(true)
|
||||||
, holdoff_counter(0)
|
, holdoff_counter(0)
|
||||||
, last_saturated(false)
|
, last_saturated(false)
|
||||||
, saturation_event_count(0)
|
, saturation_event_count(0)
|
||||||
|
|||||||
@@ -2180,24 +2180,9 @@ int main(void)
|
|||||||
|
|
||||||
runRadarPulseSequence();
|
runRadarPulseSequence();
|
||||||
|
|
||||||
/* [AGC] Outer-loop AGC: sync enable from FPGA via DIG_6 (PD14),
|
/* [AGC] Outer-loop AGC: read FPGA saturation flag (DIG_5 / PD13),
|
||||||
* then read saturation flag (DIG_5 / PD13) and adjust ADAR1000 VGA
|
* adjust ADAR1000 VGA common gain once per radar frame (~258 ms).
|
||||||
* common gain once per radar frame (~258 ms).
|
* Only run when AGC is enabled — otherwise leave VGA gains untouched. */
|
||||||
* 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) {
|
if (outerAgc.enabled) {
|
||||||
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
bool sat = HAL_GPIO_ReadPin(FPGA_DIG5_SAT_GPIO_Port,
|
||||||
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
FPGA_DIG5_SAT_Pin) == GPIO_PIN_SET;
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ static void test_defaults()
|
|||||||
assert(agc.min_gain == 0);
|
assert(agc.min_gain == 0);
|
||||||
assert(agc.max_gain == 127);
|
assert(agc.max_gain == 127);
|
||||||
assert(agc.holdoff_frames == 4);
|
assert(agc.holdoff_frames == 4);
|
||||||
assert(agc.enabled == false); // disabled by default — FPGA DIG_6 is source of truth
|
assert(agc.enabled == true);
|
||||||
assert(agc.holdoff_counter == 0);
|
assert(agc.holdoff_counter == 0);
|
||||||
assert(agc.last_saturated == false);
|
assert(agc.last_saturated == false);
|
||||||
assert(agc.saturation_event_count == 0);
|
assert(agc.saturation_event_count == 0);
|
||||||
@@ -67,7 +67,6 @@ static void test_defaults()
|
|||||||
static void test_saturation_reduces_gain()
|
static void test_saturation_reduces_gain()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
uint8_t initial = agc.agc_base_gain; // 30
|
uint8_t initial = agc.agc_base_gain; // 30
|
||||||
|
|
||||||
agc.update(true); // saturation
|
agc.update(true); // saturation
|
||||||
@@ -83,7 +82,6 @@ static void test_saturation_reduces_gain()
|
|||||||
static void test_holdoff_prevents_early_gain_up()
|
static void test_holdoff_prevents_early_gain_up()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.update(true); // saturate once -> gain = 26
|
agc.update(true); // saturate once -> gain = 26
|
||||||
uint8_t after_sat = agc.agc_base_gain;
|
uint8_t after_sat = agc.agc_base_gain;
|
||||||
|
|
||||||
@@ -103,7 +101,6 @@ static void test_holdoff_prevents_early_gain_up()
|
|||||||
static void test_recovery_after_holdoff()
|
static void test_recovery_after_holdoff()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.update(true); // saturate -> gain = 26
|
agc.update(true); // saturate -> gain = 26
|
||||||
uint8_t after_sat = agc.agc_base_gain;
|
uint8_t after_sat = agc.agc_base_gain;
|
||||||
|
|
||||||
@@ -122,7 +119,6 @@ static void test_recovery_after_holdoff()
|
|||||||
static void test_min_gain_clamp()
|
static void test_min_gain_clamp()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.min_gain = 10;
|
agc.min_gain = 10;
|
||||||
agc.agc_base_gain = 12;
|
agc.agc_base_gain = 12;
|
||||||
agc.gain_step_down = 4;
|
agc.gain_step_down = 4;
|
||||||
@@ -140,7 +136,6 @@ static void test_min_gain_clamp()
|
|||||||
static void test_max_gain_clamp()
|
static void test_max_gain_clamp()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.max_gain = 32;
|
agc.max_gain = 32;
|
||||||
agc.agc_base_gain = 31;
|
agc.agc_base_gain = 31;
|
||||||
agc.gain_step_up = 2;
|
agc.gain_step_up = 2;
|
||||||
@@ -231,7 +226,6 @@ static void test_apply_gain_spi()
|
|||||||
static void test_reset_preserves_config()
|
static void test_reset_preserves_config()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.agc_base_gain = 42;
|
agc.agc_base_gain = 42;
|
||||||
agc.gain_step_down = 8;
|
agc.gain_step_down = 8;
|
||||||
agc.cal_offset[3] = -5;
|
agc.cal_offset[3] = -5;
|
||||||
@@ -261,7 +255,6 @@ static void test_reset_preserves_config()
|
|||||||
static void test_saturation_counter()
|
static void test_saturation_counter()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; ++i) {
|
for (int i = 0; i < 10; ++i) {
|
||||||
agc.update(true);
|
agc.update(true);
|
||||||
@@ -281,7 +274,6 @@ static void test_saturation_counter()
|
|||||||
static void test_mixed_sequence()
|
static void test_mixed_sequence()
|
||||||
{
|
{
|
||||||
ADAR1000_AGC agc;
|
ADAR1000_AGC agc;
|
||||||
agc.enabled = true; // default is OFF; enable for this test
|
|
||||||
agc.agc_base_gain = 30;
|
agc.agc_base_gain = 30;
|
||||||
agc.gain_step_down = 4;
|
agc.gain_step_down = 4;
|
||||||
agc.gain_step_up = 1;
|
agc.gain_step_up = 1;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 = H11, DIG_6 = G12, DIG_7 = H12 — FPGA→STM32 status outputs
|
||||||
# DIG_5: AGC saturation flag (PD13 on STM32)
|
# DIG_5: AGC saturation flag (PD13 on STM32)
|
||||||
# DIG_6: AGC enable flag (PD14) — mirrors FPGA host_agc_enable to STM32
|
# DIG_6: reserved (PD14)
|
||||||
# DIG_7: reserved (PD15)
|
# DIG_7: reserved (PD15)
|
||||||
set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}]
|
set_property PACKAGE_PIN H11 [get_ports {gpio_dig5}]
|
||||||
set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}]
|
set_property PACKAGE_PIN G12 [get_ports {gpio_dig6}]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -130,7 +130,7 @@ module radar_system_top (
|
|||||||
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
|
// FPGA→STM32 GPIO outputs (DIG_5..DIG_7 on 50T board)
|
||||||
// Used by STM32 outer AGC loop to read saturation state without USB polling.
|
// 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_dig5, // DIG_5 (H11→PD13): AGC saturation flag (1=clipping detected)
|
||||||
output wire gpio_dig6, // DIG_6 (G12→PD14): AGC enable flag (mirrors host_agc_enable)
|
output wire gpio_dig6, // DIG_6 (G12→PD14): reserved (tied low)
|
||||||
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
output wire gpio_dig7 // DIG_7 (H12→PD15): reserved (tied low)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1037,11 +1037,9 @@ assign system_status = status_reg;
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0.
|
// DIG_5: AGC saturation flag — high when per-frame saturation_count > 0.
|
||||||
// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain.
|
// STM32 reads PD13 to detect clipping and adjust ADAR1000 VGA gain.
|
||||||
// DIG_6: AGC enable flag — mirrors host_agc_enable so STM32 outer-loop AGC
|
// DIG_6, DIG_7: Reserved (tied low for future use).
|
||||||
// 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_dig5 = (rx_agc_saturation_count != 8'd0);
|
||||||
assign gpio_dig6 = host_agc_enable;
|
assign gpio_dig6 = 1'b0;
|
||||||
assign gpio_dig7 = 1'b0;
|
assign gpio_dig7 = 1'b0;
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
# ===========================================================================
|
# ===========================================================================
|
||||||
|
|||||||
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
|
||||||
@@ -27,7 +27,6 @@ layers agree (because both could be wrong).
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import struct
|
import struct
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -370,188 +369,6 @@ 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:
|
class TestTier1DataPacketLayout:
|
||||||
"""Verify data packet byte layout matches between Python and Verilog."""
|
"""Verify data packet byte layout matches between Python and Verilog."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user