fix(fpga): pre-bringup RTL hardening + test-suite hardening

RTL (P0 pre-bringup findings R-1/R-2/R-3/R-5/R-6):

- mti_canceller: add use_long_chirp input and waveform-boundary mute
  so the long->short transition in mode 01 no longer subtracts across
  heterogeneous waveforms (R-1). Prev buffer is overwritten in-flight
  at the boundary so the next same-waveform chirp subtracts cleanly.
- ad9484_interface_400m: 2FF sync of mmcm_locked into the 400 MHz
  domain before gating reset_n_gated (R-6).
- cic_decimator_4x_enhanced: correct max_fanout narrative (R-3).
- ad9484_interface_400m: strip stale pblock comment, note 3.0 ns
  max_delay instead (R-2).
- mti_canceller / doppler_processor: 200T-20km WARNING banners
  flagging the broken 4096-bin path (R-5). 9-bit BRAM address aliases
  silently until rewritten.
- adc_clk_mmcm.xdc: relax set_max_delay from 2.700 -> 3.000 ns,
  closes WNS with headroom on 50T build.
- radar_receiver_final: wire use_long_chirp into mti_inst.

Architecture-bump finalization (2048-pt range FFT, 512 range bins,
32 Doppler bins -> 16384 output cells per frame):

- tb/cosim/radar_scene.py: FFT_SIZE 1024 -> 2048, RANGE_BINS 64 -> 512.
- tb/gen_mf_golden_ref.py: N 1024 -> 2048.
- Regenerate all affected hex goldens (MF cases 1-4, Doppler inputs
  + py goldens, receiver integration golden_doppler.mem 2048 -> 16384).
- tb_radar_receiver_final: widen range_bin_out 6 -> 9 bits, bump
  GOLDEN_ENTRIES 2048 -> 16384, expand bitmaps/arrays to 512 bins,
  update all check messages and thresholds.
- tb_mti_canceller, tb_fullchain_mti_cfar_realdata: tie/pass
  use_long_chirp so compile still works after RTL port add.

Test-suite hardening (coverage audit findings):

- tb_mti_canceller T12: 10 new assertions exercising R-1 waveform-
  boundary mute across a long/long/short/short/long sequence. Catches
  a regression that re-enables subtraction across the boundary.
- tb_fir_lowpass: replace tautological check(1'b1, ...) on coefficient
  symmetry with a real hierarchical check coeff[k]===coeff[31-k];
  replace always-pass overflow check with a well-driven (not X/Z)
  assertion on filter_overflow.
- tb_matched_filter_processing_chain: replace three always-pass peak-
  bin placeholders with peak-to-mean-|out| > 2x ratio checks (catches
  flat/zero output that the old tautologies silently accepted).
- tb_cdc_modules M2: replace always-pass narrow-pulse check with a
  well-defined-output assertion on the synchronizer.
- tb_nco_400m: replace always-pass freq-switch check with a swing +
  no-X assertion across 200 post-switch samples.
- tb_system_e2e G12.1: replace check(1, ...) with test_num > 20 so
  it catches a stalled TB that skipped prior groups.
- tb_multiseg_cosim TEST 4: replace always-pass placeholder with a
  bitmap that asserts segment_request visited all 4 values.
- tb_mf_chain_synth and tb_fullchain_mti_cfar_realdata: add DEPRECATED
  headers plus \$fatal guards (ifndef ALLOW_STALE_*) so they cannot
  be silently re-enabled in CI with stale 1024-bin goldens against
  current 2048-pt RTL.

Regression: 32 passed, 0 failed. MTI TB grew 30 -> 39 checks;
receiver integration grew 17 -> 18 checks with 16384/16384 golden
match at tolerance +/- 2 LSB.
This commit is contained in:
Jason
2026-04-22 13:23:38 +05:45
parent c668652ba8
commit 8865e9a0ef
54 changed files with 204966 additions and 35813 deletions
+24 -1
View File
@@ -118,6 +118,14 @@ endgenerate
// frequency-matched. This single register stage transfers from IOB (BUFIO) // frequency-matched. This single register stage transfers from IOB (BUFIO)
// to fabric (BUFG) with guaranteed timing. // to fabric (BUFG) with guaranteed timing.
// ============================================================================ // ============================================================================
// Timing on the BUFIOBUFG CDC edge is governed by a 3.000 ns
// set_max_delay in constraints/adc_clk_mmcm.xdc (1.2× the 2.500 ns period),
// which leaves the placer free and still fits inside the ADC data-valid
// window. IOB=TRUE and a pblock around the IDDR column were both tried
// and rejected: IOB packing fails because the BUFG clock on these
// capture FFs can't share the ILOGIC clock mux with the BUFIO-clocked
// IDDR, and the pblock pulled fanout logic into the I/O region and
// triggered router congestion on 51 unrelated paths.
reg [7:0] adc_data_rise_bufg; reg [7:0] adc_data_rise_bufg;
reg [7:0] adc_data_fall_bufg; reg [7:0] adc_data_fall_bufg;
@@ -139,9 +147,24 @@ reg dco_phase;
// //
// mmcm_locked gates de-assertion: the 400 MHz domain stays in reset until // mmcm_locked gates de-assertion: the 400 MHz domain stays in reset until
// the MMCM PLL has locked and the jitter-cleaned clock is stable. // the MMCM PLL has locked and the jitter-cleaned clock is stable.
// mmcm_locked is a combinational MMCME2 output and can glitch; sync it
// into the 400 MHz domain with a 2-FF chain before using it in the
// async-reset branch below so a LOCKED blip doesn't asynchronously
// re-reset the domain. The chain is itself async-reset by the raw
// reset_n so it forces reset_n_gated=0 at power-up (no valid adc_dco
// edges exist yet to clock the sync chain).
(* ASYNC_REG = "TRUE" *) reg [1:0] mmcm_locked_sync_400m;
always @(posedge adc_dco_buffered or negedge reset_n) begin
if (!reset_n)
mmcm_locked_sync_400m <= 2'b00;
else
mmcm_locked_sync_400m <= {mmcm_locked_sync_400m[0], mmcm_locked};
end
wire mmcm_locked_400m = mmcm_locked_sync_400m[1];
(* ASYNC_REG = "TRUE" *) reg [1:0] reset_sync_400m; (* ASYNC_REG = "TRUE" *) reg [1:0] reset_sync_400m;
wire reset_n_400m; wire reset_n_400m;
wire reset_n_gated = reset_n & mmcm_locked; wire reset_n_gated = reset_n & mmcm_locked_400m;
always @(posedge adc_dco_buffered or negedge reset_n_gated) begin always @(posedge adc_dco_buffered or negedge reset_n_gated) begin
if (!reset_n_gated) if (!reset_n_gated)
@@ -74,7 +74,15 @@ localparam COMB_WIDTH = 28;
// DSP output) = 4 cycles at 400 MHz = 10 ns. // DSP output) = 4 cycles at 400 MHz = 10 ns.
// Negligible vs system reset assertion duration. // Negligible vs system reset assertion duration.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
(* max_fanout = 25 *) reg reset_h = 1'b1; // INIT=1'b1: registers start in reset state on power-up // max_fanout = 16 (reduced from 25): forces Vivado to replicate reset_h into
// ~45 copies instead of ~28 across the DSP48E1 RST* + fabric loads, so each
// replica drives a smaller cluster and places closer to its loads. Kept
// because it shortens the longest reset_h_reg_rep__*/C → integrator_*/RSTP
// route on a 95%-packed XC7A50T, but NOTE: the 52 ps WNS miss that first
// motivated this tweak was ultimately closed by relaxing the BUFIO↔MMCM
// set_max_delay from 2.700 ns to 3.000 ns in constraints/adc_clk_mmcm.xdc,
// not by the fan-out change alone.
(* max_fanout = 16 *) reg reset_h = 1'b1; // INIT=1'b1: registers start in reset state on power-up
always @(posedge clk) reset_h <= ~reset_n; always @(posedge clk) reset_h <= ~reset_n;
// Sign-extended input for integrator_0 C port (48-bit) // Sign-extended input for integrator_0 C port (48-bit)
@@ -30,13 +30,25 @@
# into the MMCM BUFG domain in ad9484_interface_400m.v. # into the MMCM BUFG domain in ad9484_interface_400m.v.
# These clocks are frequency-matched and phase-related (MMCM is locked to # These clocks are frequency-matched and phase-related (MMCM is locked to
# adc_dco_p), so the single register transfer is safe. We use max_delay # adc_dco_p), so the single register transfer is safe. We use max_delay
# (one period) to ensure the tools verify the transfer fits within one cycle # to ensure the tools verify the transfer fits within the valid data window
# without over-constraining with full inter-clock setup/hold analysis. # without over-constraining with full inter-clock setup/hold analysis.
#
# 3.000 ns = 1.2× the 2.500 ns clock period. On a 95%-packed XC7A50T the
# placer cannot keep the capture FFs (adc_data_{rise,fall}_bufg) next to
# the IDDR column (observed routes ~2.28 ns IDDR → SLICE_X0Y123); the old
# 2.700 ns window failed by ~120 ps. A pblock attempt pulled fanout logic
# into the I/O region and triggered router-congestion on 51 other paths,
# confirming that the right lever is the constraint, not placement.
# 3.000 ns is safe: (a) IDDR Q outputs are valid for ~1 full adc_dco_p
# period, (b) MMCM-locked phase relation keeps launch/capture edges
# deterministic, (c) 0 logic levels on the datapath, (d) even with worst-
# case route and skew, 300 ps of extra budget still fits inside the ADC
# output-valid window (AD9484 datasheet: data valid 100 ps after DCO edge).
set_max_delay -datapath_only -from [get_clocks adc_dco_p] \ set_max_delay -datapath_only -from [get_clocks adc_dco_p] \
-to [get_clocks clk_mmcm_out0] 2.700 -to [get_clocks clk_mmcm_out0] 3.000
set_max_delay -datapath_only -from [get_clocks clk_mmcm_out0] \ set_max_delay -datapath_only -from [get_clocks clk_mmcm_out0] \
-to [get_clocks adc_dco_p] 2.700 -to [get_clocks adc_dco_p] 3.000
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# CDC: MMCM output domain ↔ other clock domains # CDC: MMCM output domain ↔ other clock domains
+13
View File
@@ -34,6 +34,19 @@
`include "radar_params.vh" `include "radar_params.vh"
// ----------------------------------------------------------------------------
// !!! 200T 20 km MODE BROKEN — FIX BEFORE 200T BRING-UP !!!
// RANGE_BINS and the range_bin output port default to `RP_NUM_RANGE_BINS
// (512) / `RP_RANGE_BIN_BITS (9). In 20 km mode the upstream pipeline
// emits `RP_OUTPUT_RANGE_BINS_20KM = 4096 bins/chirp, which the internal
// range-bin BRAMs and address counters here cannot represent — bins
// 512..4095 alias onto bins 0..511 and the Doppler FFT collects a
// scrambled slow-time vector per aliased range cell.
// Latent on XC7A50T (SUPPORT_LONG_RANGE undefined → 3 km only); will
// corrupt all 20 km output on XC7A200T. Before 200T bring-up: scale
// RANGE_BINS with `RP_MAX_OUTPUT_BINS, widen range_bin, and resize the
// per-range chirp buffers, or route 20 km mode around this block.
// ----------------------------------------------------------------------------
module doppler_processor_optimized #( module doppler_processor_optimized #(
parameter DOPPLER_FFT_SIZE = `RP_DOPPLER_FFT_SIZE, // 16 parameter DOPPLER_FFT_SIZE = `RP_DOPPLER_FFT_SIZE, // 16
parameter RANGE_BINS = `RP_NUM_RANGE_BINS, // 512 parameter RANGE_BINS = `RP_NUM_RANGE_BINS, // 512
+53 -5
View File
@@ -43,6 +43,19 @@
`include "radar_params.vh" `include "radar_params.vh"
// ----------------------------------------------------------------------------
// !!! 200T 20 km MODE BROKEN — FIX BEFORE 200T BRING-UP !!!
// The prev-chirp BRAM buffer is sized to NUM_RANGE_BINS (512) and the
// range_bin_in port is 9 bits (`RP_RANGE_BIN_BITS). In 20 km mode the
// upstream range_bin_decimator emits `RP_OUTPUT_RANGE_BINS_20KM = 4096
// bins per chirp (8 segments × 512 decimated bins), which aliases into
// the 9-bit address space and collapses bins 512..4095 onto bins 0..511.
// On XC7A50T this is latent (SUPPORT_LONG_RANGE undefined → 3 km only),
// but on XC7A200T with SUPPORT_LONG_RANGE the 20 km data path will
// silently corrupt every range cell above 3 km.
// Fix before 200T bring-up: scale NUM_RANGE_BINS/range_bin width with
// `RP_MAX_OUTPUT_BINS, or gate MTI off entirely in 20 km mode.
// ----------------------------------------------------------------------------
module mti_canceller #( module mti_canceller #(
parameter NUM_RANGE_BINS = `RP_NUM_RANGE_BINS, // 512 parameter NUM_RANGE_BINS = `RP_NUM_RANGE_BINS, // 512
parameter DATA_WIDTH = `RP_DATA_WIDTH // 16 parameter DATA_WIDTH = `RP_DATA_WIDTH // 16
@@ -65,6 +78,14 @@ module mti_canceller #(
// ========== CONFIGURATION ========== // ========== CONFIGURATION ==========
input wire mti_enable, // 1=MTI active, 0=pass-through input wire mti_enable, // 1=MTI active, 0=pass-through
// Current chirp's waveform selector (from radar_mode_controller). Used
// to mute MTI output across the long↔short chirp boundary in range
// mode 01 (long-range interleave) — without this, the first chirp of
// a new waveform subtracts the previous waveform's range profile,
// injecting a per-range-bin impulse into slow-time sample 0 of the
// new Doppler sub-frame that spreads across all Doppler bins.
input wire use_long_chirp,
// ========== STATUS ========== // ========== STATUS ==========
output reg mti_first_chirp, // 1 during first chirp (output muted) output reg mti_first_chirp, // 1 during first chirp (output muted)
@@ -92,6 +113,7 @@ reg signed [DATA_WIDTH-1:0] range_i_d1, range_q_d1;
reg range_valid_d1; reg range_valid_d1;
reg [`RP_RANGE_BIN_BITS-1:0] range_bin_d1; reg [`RP_RANGE_BIN_BITS-1:0] range_bin_d1;
reg mti_enable_d1; reg mti_enable_d1;
reg use_long_chirp_d1;
always @(posedge clk or negedge reset_n) begin always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin if (!reset_n) begin
@@ -100,12 +122,14 @@ always @(posedge clk or negedge reset_n) begin
range_valid_d1 <= 1'b0; range_valid_d1 <= 1'b0;
range_bin_d1 <= {`RP_RANGE_BIN_BITS{1'b0}}; range_bin_d1 <= {`RP_RANGE_BIN_BITS{1'b0}};
mti_enable_d1 <= 1'b0; mti_enable_d1 <= 1'b0;
use_long_chirp_d1 <= 1'b0;
end else begin end else begin
range_i_d1 <= range_i_in; range_i_d1 <= range_i_in;
range_q_d1 <= range_q_in; range_q_d1 <= range_q_in;
range_valid_d1 <= range_valid_in; range_valid_d1 <= range_valid_in;
range_bin_d1 <= range_bin_in; range_bin_d1 <= range_bin_in;
mti_enable_d1 <= mti_enable; mti_enable_d1 <= mti_enable;
use_long_chirp_d1 <= use_long_chirp;
end end
end end
@@ -139,6 +163,14 @@ end
// Track whether we have valid previous data // Track whether we have valid previous data
reg has_previous; reg has_previous;
// Waveform of the chirp whose profile currently lives in prev_i/prev_q.
// Latched at end-of-chirp when we mark has_previous=1. Compared against
// the incoming chirp's waveform at its first bin (range_bin_d1 == 0) to
// detect a long↔short transition and re-mute.
reg prev_chirp_was_long;
wire waveform_changed = has_previous
&& (use_long_chirp_d1 != prev_chirp_was_long);
// ============================================================================ // ============================================================================
// MTI PROCESSING (operates on d1 pipeline stage + BRAM read data) // MTI PROCESSING (operates on d1 pipeline stage + BRAM read data)
// ============================================================================ // ============================================================================
@@ -182,6 +214,7 @@ always @(posedge clk or negedge reset_n) begin
range_bin_out <= {`RP_RANGE_BIN_BITS{1'b0}}; range_bin_out <= {`RP_RANGE_BIN_BITS{1'b0}};
has_previous <= 1'b0; has_previous <= 1'b0;
mti_first_chirp <= 1'b1; mti_first_chirp <= 1'b1;
prev_chirp_was_long <= 1'b0;
mti_saturation_count <= 8'd0; mti_saturation_count <= 8'd0;
end else begin end else begin
// Count saturated MTI-active samples (F-6.3). Clamp at 0xFF. // Count saturated MTI-active samples (F-6.3). Clamp at 0xFF.
@@ -206,24 +239,39 @@ always @(posedge clk or negedge reset_n) begin
// Reset first-chirp state when MTI is disabled // Reset first-chirp state when MTI is disabled
has_previous <= 1'b0; has_previous <= 1'b0;
mti_first_chirp <= 1'b1; mti_first_chirp <= 1'b1;
end else if (!has_previous) begin end else if (!has_previous || waveform_changed) begin
// First chirp after enable: mute output (no subtraction possible). // No valid previous chirp to subtract from — either the very
// Still emit valid=1 with zero data so Doppler processor gets // first chirp after reset/enable, or the long↔short boundary
// the expected number of samples per frame. // in range_mode=01 where the prev buffer holds a different
// waveform's profile. Mute output (emit zeros with valid=1
// so Doppler still sees the expected chirp count), overwrite
// prev_i/prev_q as this chirp streams through the write port,
// then re-arm at end-of-chirp with the CURRENT waveform tag.
range_i_out <= {DATA_WIDTH{1'b0}}; range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}}; range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b1; range_valid_out <= 1'b1;
mti_first_chirp <= 1'b1;
// After last range bin of first chirp, mark previous as valid // After last range bin of this chirp, the prev buffer now
// holds a full copy of THIS chirp's profile — arm for the
// next chirp and remember which waveform was written.
if (range_bin_d1 == NUM_RANGE_BINS - 1) begin if (range_bin_d1 == NUM_RANGE_BINS - 1) begin
has_previous <= 1'b1; has_previous <= 1'b1;
mti_first_chirp <= 1'b0; mti_first_chirp <= 1'b0;
prev_chirp_was_long <= use_long_chirp_d1;
end end
end else begin end else begin
// Normal MTI: subtract previous from current // Normal MTI: subtract previous from current
range_i_out <= diff_i_sat; range_i_out <= diff_i_sat;
range_q_out <= diff_q_sat; range_q_out <= diff_q_sat;
range_valid_out <= 1'b1; range_valid_out <= 1'b1;
// Refresh the waveform tag at end-of-chirp so the compare
// on the next chirp stays correct (same-waveform runs are
// the common case and the tag must track them).
if (range_bin_d1 == NUM_RANGE_BINS - 1) begin
prev_chirp_was_long <= use_long_chirp_d1;
end
end end
end end
end end
@@ -485,6 +485,7 @@ mti_canceller #(
.range_valid_out(mti_range_valid), .range_valid_out(mti_range_valid),
.range_bin_out(mti_range_bin), .range_bin_out(mti_range_bin),
.mti_enable(host_mti_enable), .mti_enable(host_mti_enable),
.use_long_chirp(use_long_chirp),
.mti_first_chirp(mti_first_chirp), .mti_first_chirp(mti_first_chirp),
.mti_saturation_count(mti_saturation_count_out) .mti_saturation_count(mti_saturation_count_out)
); );
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -52,9 +52,11 @@ T_PRI_SHORT = 175e-6 # staggered short-PRI sub-frame
N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples N_SAMPLES_LISTEN = int(T_LISTEN_LONG * FS_ADC) # 54800 samples
# Processing chain # Processing chain
# Updated for 2048-pt range FFT + 4x decimation → 512 range bins per chirp.
# Must stay in sync with radar_params.vh: RP_FFT_SIZE=2048, RP_NUM_RANGE_BINS=512.
CIC_DECIMATION = 4 CIC_DECIMATION = 4
FFT_SIZE = 1024 FFT_SIZE = 2048
RANGE_BINS = 64 RANGE_BINS = 512
DOPPLER_FFT_SIZE = 16 # Per sub-frame DOPPLER_FFT_SIZE = 16 # Per sub-frame
DOPPLER_TOTAL_BINS = 32 # Total output bins (2 sub-frames x 16) DOPPLER_TOTAL_BINS = 32 # Total output bins (2 sub-frames x 16)
CHIRPS_PER_SUBFRAME = 16 CHIRPS_PER_SUBFRAME = 16
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -22,7 +22,7 @@ Usage:
import os import os
import numpy as np import numpy as np
N = 1024 # FFT length N = 2048 # FFT length — matches `RP_FFT_SIZE` (radar_params.vh)
def to_q15(value): def to_q15(value):
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,15 +1,15 @@
======================================================================== ========================================================================
Matched Filter Golden Reference Summary Matched Filter Golden Reference Summary
Operation: output = IFFT( FFT(signal) * conj(FFT(reference)) ) Operation: output = IFFT( FFT(signal) * conj(FFT(reference)) )
FFT length: 1024 FFT length: 2048
======================================================================== ========================================================================
------------------------------------------------------------------------ ------------------------------------------------------------------------
Case 1: DC autocorrelation: signal=ref=DC(I=0x1000,Q=0). Expected: large peak at bin 0, zero elsewhere. Peak will saturate to 32767 due to 16-bit clamp. Case 1: DC autocorrelation: signal=ref=DC(I=0x1000,Q=0). Expected: large peak at bin 0, zero elsewhere. Peak will saturate to 32767 due to 16-bit clamp.
------------------------------------------------------------------------ ------------------------------------------------------------------------
Peak bin: 0 Peak bin: 0
Peak magnitude (float):17179869184.000000 Peak magnitude (float):34359738368.000000
Peak I (float): 17179869184.000000 Peak I (float): 34359738368.000000
Peak Q (float): 0.000000 Peak Q (float): 0.000000
Peak I (quantized): 32767 Peak I (quantized): 32767
Peak Q (quantized): 0 Peak Q (quantized): 0
@@ -25,8 +25,8 @@ Case 1: DC autocorrelation: signal=ref=DC(I=0x1000,Q=0). Expected: large peak at
Case 2: Tone autocorrelation: signal=ref=tone(bin 5, amp 8000). Expected: peak at bin 0 (autocorrelation peak at zero lag). Case 2: Tone autocorrelation: signal=ref=tone(bin 5, amp 8000). Expected: peak at bin 0 (autocorrelation peak at zero lag).
------------------------------------------------------------------------ ------------------------------------------------------------------------
Peak bin: 0 Peak bin: 0
Peak magnitude (float):65536183223.999985 Peak magnitude (float):131072740064.000046
Peak I (float): 65536183223.999985 Peak I (float): 131072740064.000046
Peak Q (float): -0.000000 Peak Q (float): -0.000000
Peak I (quantized): 32767 Peak I (quantized): 32767
Peak Q (quantized): 0 Peak Q (quantized): 0
@@ -41,10 +41,10 @@ Case 2: Tone autocorrelation: signal=ref=tone(bin 5, amp 8000). Expected: peak a
------------------------------------------------------------------------ ------------------------------------------------------------------------
Case 3: Shifted tone: signal=tone(bin 5), ref=tone(bin 5) delayed by 3 samples. Cross-correlation peak should shift to indicate the delay. Case 3: Shifted tone: signal=tone(bin 5), ref=tone(bin 5) delayed by 3 samples. Cross-correlation peak should shift to indicate the delay.
------------------------------------------------------------------------ ------------------------------------------------------------------------
Peak bin: 253 Peak bin: 509
Peak magnitude (float):65536183223.999992 Peak magnitude (float):131072740063.999985
Peak I (float): 0.000005 Peak I (float): -0.000016
Peak Q (float): 65536183223.999992 Peak Q (float): 131072740063.999985
Peak I (quantized): 0 Peak I (quantized): 0
Peak Q (quantized): 32767 Peak Q (quantized): 32767
Files: Files:
+6 -2
View File
@@ -527,10 +527,14 @@ module tb_cdc_modules;
if (m2_dst_signal) saw_pulse = 1; if (m2_dst_signal) saw_pulse = 1;
end end
// Single src_clk pulse at 400MHz might be too short for 100MHz dst // Single src_clk pulse at 400MHz might be too short for 100MHz dst
// This is a known limitation of single-bit synchronizers // This is a known limitation of single-bit synchronizers. We do
// not assert capture (drop is acceptable for a narrow pulse) — but
// the dst-side output MUST be well-defined (0 or 1, never X/Z)
// after synchronizer settling.
$display(" Single src_clk pulse captured: %b (may miss expected for narrow pulse)", $display(" Single src_clk pulse captured: %b (may miss expected for narrow pulse)",
saw_pulse); saw_pulse);
check(1'b1, "M2: Narrow pulse test completed (miss is acceptable)"); check(m2_dst_signal === 1'b0 || m2_dst_signal === 1'b1,
"M2: Synchronizer output is well-defined (not X/Z) after narrow pulse");
end end
// ── B5: Long pulse always captured ───────────────────── // ── B5: Long pulse always captured ─────────────────────
+21 -4
View File
@@ -186,8 +186,22 @@ module tb_fir_lowpass;
output_count = output_count + 1; output_count = output_count + 1;
end end
end end
// Symmetry is inherent in the coefficient initialization // Verify linear-phase symmetry: coeff[k] == coeff[TAPS-1-k] for all k.
check(1'b1, "Coefficients are symmetric (verified from RTL source)"); // Reads the DUT's coefficient ROM directly via hierarchical reference.
begin : coeff_symmetry_check
integer sym_k;
reg sym_ok;
sym_ok = 1'b1;
for (sym_k = 0; sym_k < 16; sym_k = sym_k + 1) begin
if (uut.coeff[sym_k] !== uut.coeff[31 - sym_k]) begin
$display(" [SYM] coeff[%0d]=%h != coeff[%0d]=%h",
sym_k, uut.coeff[sym_k],
31 - sym_k, uut.coeff[31 - sym_k]);
sym_ok = 1'b0;
end
end
check(sym_ok, "Coefficients are symmetric (coeff[k] == coeff[31-k])");
end
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
// TEST GROUP 5: Low-frequency sinusoid (passband) // TEST GROUP 5: Low-frequency sinusoid (passband)
@@ -287,8 +301,11 @@ module tb_fir_lowpass;
end end
$display(" filter_overflow detected: %b", saw_nonzero); $display(" filter_overflow detected: %b", saw_nonzero);
// Note: overflow depends on coefficient sum — may or may not trigger // filter_overflow must be a valid 1-bit value at all times (not X/Z).
check(1'b1, "Overflow detection logic exists and runs"); // Whether it fires with max input depends on coefficient sum — we do
// not assert a specific polarity, but the signal must be well-driven.
check(filter_overflow === 1'b0 || filter_overflow === 1'b1,
"filter_overflow is well-driven (not X/Z) after max-input stimulus");
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
// TEST GROUP 8: data_valid gating // TEST GROUP 8: data_valid gating
@@ -3,6 +3,18 @@
/** /**
* tb_fullchain_mti_cfar_realdata.v * tb_fullchain_mti_cfar_realdata.v
* *
* ============================================================================
* DEPRECATED STALE FOR 2048-PT ARCHITECTURE (integration/fft-2048-on-p0)
* ----------------------------------------------------------------------------
* Hard-coded for INPUT_BINS=1024 / RANGE_BINS=64 / DECIM_FACTOR=16. Production
* pipeline is now 2048-pt range FFT -> 512 range bins (DECIM=4). Re-enabling
* this TB without regenerating Python goldens (golden_reference.py and the
* fullchain_*.hex files) would feed mis-sized data into current RTL and pass
* on nonsense. Do NOT wire into run_regression.sh until rewritten.
*
* Runtime guard ($fatal at startup) below prevents silent CI resurrection.
* ============================================================================
*
* Full-chain co-simulation testbench: feeds real ADI CN0566 radar data * Full-chain co-simulation testbench: feeds real ADI CN0566 radar data
* (post-range-FFT, 32 chirps x 1024 bins) through the complete signal * (post-range-FFT, 32 chirps x 1024 bins) through the complete signal
* processing pipeline: * processing pipeline:
@@ -193,6 +205,7 @@ mti_canceller #(
.range_valid_out(mti_valid_out), .range_valid_out(mti_valid_out),
.range_bin_out(mti_bin_out), .range_bin_out(mti_bin_out),
.mti_enable(1'b1), // MTI always enabled for this test .mti_enable(1'b1), // MTI always enabled for this test
.use_long_chirp(1'b0), // homogeneous-waveform stimulus in this TB
.mti_first_chirp(mti_first_chirp) .mti_first_chirp(mti_first_chirp)
); );
@@ -353,6 +366,13 @@ integer cfar_ref_idx;
integer cfar_mag_mismatches, cfar_thr_mismatches, cfar_det_mismatches; integer cfar_mag_mismatches, cfar_thr_mismatches, cfar_det_mismatches;
initial begin initial begin
`ifndef ALLOW_STALE_TB_FULLCHAIN_MTI_CFAR
// DEPRECATED guard — see file header. Stale 1024-bin constants against
// 2048-pt production RTL. Define ALLOW_STALE_TB_FULLCHAIN_MTI_CFAR only
// for archaeology; this TB does not validate current pipeline.
$display("ERROR: tb_fullchain_mti_cfar_realdata.v DEPRECATED (1024-bin). See header.");
$fatal;
`endif
// ---- Init ---- // ---- Init ----
pass_count = 0; pass_count = 0;
fail_count = 0; fail_count = 0;
@@ -334,7 +334,24 @@ module tb_matched_filter_processing_chain;
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Autocorrelation peak at bin %0d (expected near 0) - behavioral FFT noise, OK with Xilinx IP", cap_peak_bin); $display("[WARN] Autocorrelation peak at bin %0d (expected near 0) - behavioral FFT noise, OK with Xilinx IP", cap_peak_bin);
end end
check(1'b1, "Autocorrelation peak (skipped - behavioral FFT noise)"); // Behavioral Q15 FFT scatters the peak, so we cannot assert bin
// location — but the peak MUST dominate the mean magnitude. This
// catches an all-zero/flat output that the old tautology allowed.
begin : p2m_grp3
integer k, sum_abs, mean_abs;
sum_abs = 0;
for (k = 0; k < FFT_SIZE; k = k + 1) begin
sum_abs = sum_abs
+ (cap_out_i[k][15] ? -cap_out_i[k] : cap_out_i[k])
+ (cap_out_q[k][15] ? -cap_out_q[k] : cap_out_q[k]);
end
mean_abs = sum_abs / FFT_SIZE;
$display(" Peak-to-mean |out|: peak=%0d mean=%0d ratio~%0dx",
cap_max_abs, mean_abs,
(mean_abs == 0) ? 999999 : cap_max_abs / mean_abs);
check(cap_max_abs > mean_abs * 2,
"Autocorrelation peak dominates mean (>2x)");
end
check(cap_max_abs > 0, "Peak magnitude > 0"); check(cap_max_abs > 0, "Peak magnitude > 0");
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
@@ -481,7 +498,18 @@ module tb_matched_filter_processing_chain;
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Case 1: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin); $display("[WARN] Case 1: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin);
end end
check(1'b1, "Case 1: DUT peak bin (skipped - behavioral FFT noise)"); begin : p2m_case1
integer k, sum_abs, mean_abs;
sum_abs = 0;
for (k = 0; k < FFT_SIZE; k = k + 1) begin
sum_abs = sum_abs
+ (cap_out_i[k][15] ? -cap_out_i[k] : cap_out_i[k])
+ (cap_out_q[k][15] ? -cap_out_q[k] : cap_out_q[k]);
end
mean_abs = sum_abs / FFT_SIZE;
check(cap_max_abs > mean_abs * 2,
"Case 1: Peak dominates mean (>2x, catches flat/zero output)");
end
check(cap_max_abs > 0, "Case 1: Peak magnitude > 0"); check(cap_max_abs > 0, "Case 1: Peak magnitude > 0");
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
@@ -512,7 +540,18 @@ module tb_matched_filter_processing_chain;
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Case 2: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin); $display("[WARN] Case 2: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin);
end end
check(1'b1, "Case 2: DUT peak bin (skipped - behavioral FFT noise)"); begin : p2m_case2
integer k, sum_abs, mean_abs;
sum_abs = 0;
for (k = 0; k < FFT_SIZE; k = k + 1) begin
sum_abs = sum_abs
+ (cap_out_i[k][15] ? -cap_out_i[k] : cap_out_i[k])
+ (cap_out_q[k][15] ? -cap_out_q[k] : cap_out_q[k]);
end
mean_abs = sum_abs / FFT_SIZE;
check(cap_max_abs > mean_abs * 2,
"Case 2: Peak dominates mean (>2x, catches flat/zero output)");
end
check(cap_max_abs > 0, "Case 2: Peak magnitude > 0"); check(cap_max_abs > 0, "Case 2: Peak magnitude > 0");
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
+23 -5
View File
@@ -3,12 +3,22 @@
/** /**
* tb_mf_chain_synth.v * tb_mf_chain_synth.v
* *
* Testbench for the SYNTHESIS branch of matched_filter_processing_chain.v. * ============================================================================
* This is compiled WITHOUT -DSIMULATION so the `else` branch (fft_engine-based) * DEPRECATED STALE FOR 2048-PT ARCHITECTURE (integration/fft-2048-on-p0)
* is activated. * ----------------------------------------------------------------------------
* This testbench hard-codes FFT_SIZE=1024 and targets the old synthesis branch
* sized for a 1024-point FFT. Production RTL now uses 2048-pt range FFT
* (RP_FFT_SIZE=2048). Do NOT add this TB to run_regression.sh until it has
* been rewritten for 2048 samples running it as-is validates nothing against
* current RTL and will give false confidence.
* *
* The synthesis branch uses an iterative fft_engine (1024-pt, single butterfly), * Build fails fast on compile to prevent accidental resurrection.
* so processing takes ~40K+ clock cycles per frame. Timeouts are set accordingly. * ============================================================================
*
* Original description:
* Testbench for the SYNTHESIS branch of matched_filter_processing_chain.v.
* Compiled WITHOUT -DSIMULATION so the `else` (fft_engine) branch activates.
* Synthesis branch uses iterative fft_engine; ~40K+ cycles per frame.
*/ */
module tb_mf_chain_synth; module tb_mf_chain_synth;
@@ -230,6 +240,14 @@ module tb_mf_chain_synth;
$dumpfile("tb_mf_chain_synth.vcd"); $dumpfile("tb_mf_chain_synth.vcd");
$dumpvars(0, tb_mf_chain_synth); $dumpvars(0, tb_mf_chain_synth);
`ifndef ALLOW_STALE_TB_MF_CHAIN_SYNTH
// DEPRECATED guard — see file header. Refuse to run until rewritten
// for 2048-pt architecture. Define ALLOW_STALE_TB_MF_CHAIN_SYNTH to
// override (for archaeology only; does not validate current RTL).
$display("ERROR: tb_mf_chain_synth.v is DEPRECATED (1024-pt). See header.");
$fatal;
`endif
// Init // Init
clk = 0; clk = 0;
pass_count = 0; pass_count = 0;
+86
View File
@@ -34,6 +34,7 @@ reg signed [DATA_W-1:0] range_q_in;
reg range_valid_in; reg range_valid_in;
reg [5:0] range_bin_in; reg [5:0] range_bin_in;
reg mti_enable; reg mti_enable;
reg tb_use_long_chirp;
wire signed [DATA_W-1:0] range_i_out; wire signed [DATA_W-1:0] range_i_out;
wire signed [DATA_W-1:0] range_q_out; wire signed [DATA_W-1:0] range_q_out;
@@ -64,6 +65,7 @@ mti_canceller #(
.range_valid_out(range_valid_out), .range_valid_out(range_valid_out),
.range_bin_out(range_bin_out), .range_bin_out(range_bin_out),
.mti_enable(mti_enable), .mti_enable(mti_enable),
.use_long_chirp(tb_use_long_chirp), // driven by TB; T12 exercises boundary
.mti_first_chirp(mti_first_chirp) .mti_first_chirp(mti_first_chirp)
); );
@@ -92,6 +94,7 @@ task do_reset;
range_q_in = 0; range_q_in = 0;
range_valid_in = 0; range_valid_in = 0;
range_bin_in = 0; range_bin_in = 0;
tb_use_long_chirp = 1'b0; // default homogeneous waveform
repeat (5) @(posedge clk); repeat (5) @(posedge clk);
reset_n = 1; reset_n = 1;
repeat (2) @(posedge clk); repeat (2) @(posedge clk);
@@ -463,6 +466,89 @@ initial begin
check(11, "T11.1: Negative inputs: diff I = 2000", cap_i[0] == 16'sd2000); check(11, "T11.1: Negative inputs: diff I = 2000", cap_i[0] == 16'sd2000);
check(11, "T11.2: Negative inputs: diff Q = 500", cap_q[0] == 16'sd500); check(11, "T11.2: Negative inputs: diff Q = 500", cap_q[0] == 16'sd500);
// ================================================================
// T12: Waveform boundary mute (R-1)
// ----------------------------------------------------------------
// Mode 01 interleaves long and short chirps. The MTI prev-buffer
// holds the previous chirp's range profile; subtracting a short-
// waveform profile from a long-waveform one (or vice versa) would
// inject a per-range-bin impulse into slow-time sample 0, creating
// phantom targets across every Doppler bin.
//
// mti_canceller.v mutes the output at the transition and overwrites
// the prev buffer in-flight so the next chirp (same waveform as the
// transition chirp) subtracts cleanly. This test exercises that
// path — without it, the long→short boundary would silently corrupt
// slow-time data on real hardware.
// ================================================================
do_reset;
mti_enable = 1'b1;
// Chirp A (long, val=1000) — first chirp, muted by first-chirp path.
tb_use_long_chirp = 1'b1;
fork
feed_chirp_const(16'sd1000, 16'sd500);
capture_chirp;
join
check(12, "T12.1: Waveform-A first chirp: muted I",
cap_count == NUM_BINS && cap_i[0] == 16'sd0);
check(12, "T12.2: Waveform-A first chirp: muted Q",
cap_q[0] == 16'sd0);
// Chirp B (long, val=2000) — same waveform: 2000 - 1000 = 1000.
tb_use_long_chirp = 1'b1;
cap_count = 0;
fork
feed_chirp_const(16'sd2000, 16'sd1500);
capture_chirp;
join
check(12, "T12.3: Homogeneous long follow-up: I diff = 1000",
cap_i[0] == 16'sd1000);
check(12, "T12.4: Homogeneous long follow-up: Q diff = 1000",
cap_q[0] == 16'sd1000);
// Chirp C (short, val=5000) — WAVEFORM CHANGED: must mute, and the
// prev buffer must be overwritten with THIS chirp (not subtracted
// against the long-waveform chirp B). If R-1 regresses, we'd see
// 5000 - 2000 = 3000 here instead of 0.
tb_use_long_chirp = 1'b0;
cap_count = 0;
fork
feed_chirp_const(16'sd5000, 16'sd3000);
capture_chirp;
join
check(12, "T12.5: Waveform boundary (long->short): muted I (not 3000)",
cap_i[0] == 16'sd0);
check(12, "T12.6: Waveform boundary (long->short): muted Q (not 2000)",
cap_q[0] == 16'sd0);
// Chirp D (short, val=5500) — same waveform as C: 5500 - 5000 = 500.
// This proves the prev buffer was correctly overwritten with C,
// not stuck on B's long-waveform profile.
tb_use_long_chirp = 1'b0;
cap_count = 0;
fork
feed_chirp_const(16'sd5500, 16'sd3250);
capture_chirp;
join
check(12, "T12.7: Post-boundary short follow-up: I diff = 500",
cap_i[0] == 16'sd500);
check(12, "T12.8: Post-boundary short follow-up: Q diff = 250",
cap_q[0] == 16'sd250);
// Chirp E (short -> long) — another boundary, reverse direction,
// confirms muting is symmetric.
tb_use_long_chirp = 1'b1;
cap_count = 0;
fork
feed_chirp_const(16'sd9000, 16'sd4000);
capture_chirp;
join
check(12, "T12.9: Waveform boundary (short->long): muted I",
cap_i[0] == 16'sd0);
check(12, "T12.10: Waveform boundary (short->long): muted Q",
cap_q[0] == 16'sd0);
// ================================================================ // ================================================================
// SUMMARY // SUMMARY
// ================================================================ // ================================================================
+15 -5
View File
@@ -131,6 +131,15 @@ always @(posedge clk) begin
end end
end end
// One-hot bitmap of distinct segment_request values observed during the run.
// Used by TEST 4 to turn a tautological check into a real coverage assertion.
reg [3:0] seg_request_seen;
initial seg_request_seen = 4'b0000;
always @(posedge clk) begin
if (reset_n && mem_request)
seg_request_seen <= seg_request_seen | (4'b0001 << segment_request);
end
// ============================================================================ // ============================================================================
// Output capture // Output capture
// ============================================================================ // ============================================================================
@@ -586,11 +595,12 @@ initial begin
// TEST 4: Verify segment_request output // TEST 4: Verify segment_request output
// ==================================================================== // ====================================================================
$display("\n=== TEST 4: Segment Request Tracking ==="); $display("\n=== TEST 4: Segment Request Tracking ===");
// We verified segments 0-3 processed. Now check that segment_request // Verify that segment_request actually took all 4 values (0..3) during
// was correctly driven during processing. Since we can't look back // the long-chirp run, using the bitmap captured by the always-block above.
// in time, we test by re-running and monitoring segment_request. // A stuck segment_request would previously pass silently.
// For now, structural checks above suffice. $display(" segment_request bitmap: %b (bit k = value k seen)", seg_request_seen);
check(1'b1, "Segment request tracking (verified via segment transitions)"); check(seg_request_seen == 4'b1111,
"Segment request visited all 4 values (0,1,2,3) during long-chirp run");
// ==================================================================== // ====================================================================
// TEST 5: Non-zero output energy check // TEST 5: Non-zero output energy check
+17 -1
View File
@@ -233,13 +233,29 @@ module tb_nco_400m;
csv_file = $fopen("nco_freq_switch.csv", "w"); csv_file = $fopen("nco_freq_switch.csv", "w");
$fwrite(csv_file, "sample,sin_out,cos_out\n"); $fwrite(csv_file, "sample,sin_out,cos_out\n");
// Track sin/cos variance to verify NCO is actively generating a tone
// after the frequency switch (detects stuck/zero output).
begin : freq_switch_obs
integer fs_min, fs_max;
reg fs_saw_x;
fs_min = 32'sh7FFFFFFF;
fs_max = -32'sh80000000;
fs_saw_x = 1'b0;
for (sample_count = 0; sample_count < 200; sample_count = sample_count + 1) begin for (sample_count = 0; sample_count < 200; sample_count = sample_count + 1) begin
@(posedge clk_400m); #1; @(posedge clk_400m); #1;
$fwrite(csv_file, "%0d,%0d,%0d\n", $fwrite(csv_file, "%0d,%0d,%0d\n",
sample_count, sin_out, cos_out); sample_count, sin_out, cos_out);
if (^sin_out === 1'bx || ^cos_out === 1'bx) fs_saw_x = 1'b1;
if ($signed(sin_out) < fs_min) fs_min = $signed(sin_out);
if ($signed(sin_out) > fs_max) fs_max = $signed(sin_out);
end end
$fclose(csv_file); $fclose(csv_file);
check(1'b1, "Frequency switch completed without error"); $display(" After freq switch: sin swing [%0d..%0d]", fs_min, fs_max);
check(!fs_saw_x,
"Frequency switch: sin/cos outputs never X during 200 samples");
check(fs_max > fs_min,
"Frequency switch: NCO output varies (not stuck) after switch");
end
// ════════════════════════════════════════════════════════ // ════════════════════════════════════════════════════════
// TEST GROUP 6: phase_valid gating // TEST GROUP 6: phase_valid gating
@@ -34,10 +34,10 @@
// Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i // Signals: dut.ddc_out_i [17:0], dut.ddc_out_q [17:0], dut.ddc_valid_i
// Tap 2 (Doppler output) - golden compared (deterministic after MF buffering) // Tap 2 (Doppler output) - golden compared (deterministic after MF buffering)
// Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0], // Signals: doppler_output[31:0], doppler_valid, doppler_bin[4:0],
// range_bin_out[5:0] // range_bin_out[8:0]
// //
// Golden file: tb/golden/golden_doppler.mem // Golden file: tb/golden/golden_doppler.mem
// 2048 entries of 32-bit hex, indexed by range_bin*32 + doppler_bin // 16384 entries of 32-bit hex, indexed by range_bin*32 + doppler_bin
// //
// Strategy: // Strategy:
// - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives) // - Uses behavioral stub for ad9484_interface_400m (no Xilinx primitives)
@@ -130,7 +130,7 @@ end
wire [31:0] doppler_output; wire [31:0] doppler_output;
wire doppler_valid; wire doppler_valid;
wire [4:0] doppler_bin; wire [4:0] doppler_bin;
wire [5:0] range_bin_out; wire [8:0] range_bin_out;
radar_receiver_final dut ( radar_receiver_final dut (
.clk(clk_100m), .clk(clk_100m),
@@ -216,10 +216,10 @@ endtask
// ============================================================================ // ============================================================================
// GOLDEN MEMORY DECLARATIONS AND LOAD/STORE LOGIC // GOLDEN MEMORY DECLARATIONS AND LOAD/STORE LOGIC
// ============================================================================ // ============================================================================
localparam GOLDEN_ENTRIES = 2048; // 64 range bins * 32 Doppler bins localparam GOLDEN_ENTRIES = 16384; // 512 range bins * 32 Doppler bins
localparam GOLDEN_TOLERANCE = 2; // +/- 2 LSB tolerance for comparison localparam GOLDEN_TOLERANCE = 2; // +/- 2 LSB tolerance for comparison
reg [31:0] golden_doppler [0:2047]; reg [31:0] golden_doppler [0:16383];
// -- Golden comparison tracking -- // -- Golden comparison tracking --
integer golden_match_count; integer golden_match_count;
@@ -280,7 +280,7 @@ end
// ============================================================================ // ============================================================================
integer doppler_output_count; integer doppler_output_count;
integer doppler_frame_count; integer doppler_frame_count;
reg [63:0] range_bin_seen; // Bitmap: which range bins appeared reg [511:0] range_bin_seen; // Bitmap: which range bins appeared (512 bins)
reg [31:0] doppler_bin_seen; // Bitmap: which Doppler bins appeared reg [31:0] doppler_bin_seen; // Bitmap: which Doppler bins appeared
integer nonzero_output_count; integer nonzero_output_count;
reg [31:0] first_doppler_time; // Cycle when first doppler_valid appears reg [31:0] first_doppler_time; // Cycle when first doppler_valid appears
@@ -294,21 +294,21 @@ reg frame_done_prev;
integer csv_fd; integer csv_fd;
// Duplicate detection: one-hot bitmap per (range_bin, doppler_bin) // Duplicate detection: one-hot bitmap per (range_bin, doppler_bin)
// 64 range bins x 32 doppler bins = 2048 bits -> use an array of 64 x 32-bit regs // 512 range bins x 32 doppler bins = 16384 bits -> array of 512 x 32-bit regs
reg [31:0] index_seen [0:63]; reg [31:0] index_seen [0:511];
integer dup_count; integer dup_count;
// Bounds check B2: Doppler energy tracking per range bin // Bounds check B2: Doppler energy tracking per range bin
// For each range bin, track peak |I|+|Q| across all 32 Doppler bins // For each range bin, track peak |I|+|Q| across all 32 Doppler bins
// and total energy. Verifies pipeline computes non-trivial Doppler spectra. // and total energy. Verifies pipeline computes non-trivial Doppler spectra.
reg [31:0] peak_dbin_mag [0:63]; // max |I|+|Q| across all Doppler bins reg [31:0] peak_dbin_mag [0:511]; // max |I|+|Q| across all Doppler bins
reg [31:0] total_dbin_energy [0:63]; // sum of |I|+|Q| across all 32 Doppler bins reg [31:0] total_dbin_energy [0:511]; // sum of |I|+|Q| across all 32 Doppler bins
integer b2_init_idx; integer b2_init_idx;
initial begin initial begin
doppler_output_count = 0; doppler_output_count = 0;
doppler_frame_count = 0; doppler_frame_count = 0;
range_bin_seen = 64'd0; range_bin_seen = 512'd0;
doppler_bin_seen = 32'd0; doppler_bin_seen = 32'd0;
nonzero_output_count = 0; nonzero_output_count = 0;
first_doppler_seen = 0; first_doppler_seen = 0;
@@ -317,7 +317,7 @@ initial begin
frame_done_prev = 0; frame_done_prev = 0;
dup_count = 0; dup_count = 0;
for (b2_init_idx = 0; b2_init_idx < 64; b2_init_idx = b2_init_idx + 1) begin for (b2_init_idx = 0; b2_init_idx < 512; b2_init_idx = b2_init_idx + 1) begin
index_seen[b2_init_idx] = 32'd0; index_seen[b2_init_idx] = 32'd0;
peak_dbin_mag[b2_init_idx] = 32'd0; peak_dbin_mag[b2_init_idx] = 32'd0;
total_dbin_energy[b2_init_idx] = 32'd0; total_dbin_energy[b2_init_idx] = 32'd0;
@@ -345,8 +345,8 @@ always @(posedge clk_100m) begin
frame_output_count = frame_output_count + 1; frame_output_count = frame_output_count + 1;
// Track which bins we've seen // Track which bins we've seen
if (range_bin_out < 64) if (range_bin_out < 512)
range_bin_seen = range_bin_seen | (64'd1 << range_bin_out); range_bin_seen = range_bin_seen | (512'd1 << range_bin_out);
if (doppler_bin < 32) if (doppler_bin < 32)
doppler_bin_seen = doppler_bin_seen | (32'd1 << doppler_bin); doppler_bin_seen = doppler_bin_seen | (32'd1 << doppler_bin);
@@ -373,7 +373,7 @@ always @(posedge clk_100m) begin
gidx = range_bin_out * 32 + doppler_bin; gidx = range_bin_out * 32 + doppler_bin;
// ---- Duplicate detection (B5) ---- // ---- Duplicate detection (B5) ----
if (range_bin_out < 64 && doppler_bin < 32) begin if (range_bin_out < 512 && doppler_bin < 32) begin
if (index_seen[range_bin_out][doppler_bin]) begin if (index_seen[range_bin_out][doppler_bin]) begin
dup_count = dup_count + 1; dup_count = dup_count + 1;
if (dup_count <= 10) if (dup_count <= 10)
@@ -390,7 +390,7 @@ always @(posedge clk_100m) begin
mag_q = (mag_q_signed < 0) ? -mag_q_signed : mag_q_signed; mag_q = (mag_q_signed < 0) ? -mag_q_signed : mag_q_signed;
mag_sum = mag_i + mag_q; mag_sum = mag_i + mag_q;
if (range_bin_out < 64) begin if (range_bin_out < 512) begin
total_dbin_energy[range_bin_out] = total_dbin_energy[range_bin_out] + mag_sum; total_dbin_energy[range_bin_out] = total_dbin_energy[range_bin_out] + mag_sum;
if (mag_sum > peak_dbin_mag[range_bin_out]) if (mag_sum > peak_dbin_mag[range_bin_out])
peak_dbin_mag[range_bin_out] = mag_sum; peak_dbin_mag[range_bin_out] = mag_sum;
@@ -523,7 +523,7 @@ end
// 1. DDC pipeline fill: ~4 sys_clk cycles // 1. DDC pipeline fill: ~4 sys_clk cycles
// 2. MF overlap-save buffer fill: 896 valid DDC samples // 2. MF overlap-save buffer fill: 896 valid DDC samples
// 3. Latency buffer priming: 3187 valid_in assertions // 3. Latency buffer priming: 3187 valid_in assertions
// 4. 1024 MF outputs -> range_bin_decimator -> 64 decimated outputs // 4. 2048 MF outputs -> range_bin_decimator -> 512 decimated outputs
// 5. 32 chirps of decimated data -> Doppler FFT // 5. 32 chirps of decimated data -> Doppler FFT
// //
// With shortened mode controller timing (~600 cycles per chirp pair), // With shortened mode controller timing (~600 cycles per chirp pair),
@@ -619,24 +619,24 @@ initial begin
check(doppler_output_count > 0, check(doppler_output_count > 0,
"S4: Doppler processor produces outputs (doppler_valid asserted)"); "S4: Doppler processor produces outputs (doppler_valid asserted)");
// ---- CHECK S5: Correct output count per frame (legacy: >= 2048) ---- // ---- CHECK S5: Correct output count per frame (>= 16384) ----
if (doppler_frame_count > 0) begin if (doppler_frame_count > 0) begin
check(doppler_output_count >= 2048, check(doppler_output_count >= 16384,
"S5: At least 2048 doppler outputs (one full frame: 64 rbins x 32 dbins)"); "S5: At least 16384 doppler outputs (one full frame: 512 rbins x 32 dbins)");
end else begin end else begin
check(0, "S5: At least 2048 doppler outputs (NO FRAME COMPLETED)"); check(0, "S5: At least 16384 doppler outputs (NO FRAME COMPLETED)");
end end
// ---- CHECK S6: Range bin coverage ---- // ---- CHECK S6: Range bin coverage ----
begin : count_range_bins begin : count_range_bins
integer rb_count, rb_i; integer rb_count, rb_i;
rb_count = 0; rb_count = 0;
for (rb_i = 0; rb_i < 64; rb_i = rb_i + 1) begin for (rb_i = 0; rb_i < 512; rb_i = rb_i + 1) begin
if (range_bin_seen[rb_i]) rb_count = rb_count + 1; if (range_bin_seen[rb_i]) rb_count = rb_count + 1;
end end
$display("[INFO] Unique range bins seen: %0d / 64", rb_count); $display("[INFO] Unique range bins seen: %0d / 512", rb_count);
check(rb_count == 64, check(rb_count == 512,
"S6: All 64 range bins present in Doppler output"); "S6: All 512 range bins present in Doppler output");
end end
// ---- CHECK S7: Doppler bin coverage ---- // ---- CHECK S7: Doppler bin coverage ----
@@ -719,7 +719,7 @@ initial begin
nontrivial_count = 0; nontrivial_count = 0;
min_peak = 32'h7FFFFFFF; min_peak = 32'h7FFFFFFF;
max_peak = 0; max_peak = 0;
for (b2_rb = 0; b2_rb < 64; b2_rb = b2_rb + 1) begin for (b2_rb = 0; b2_rb < 512; b2_rb = b2_rb + 1) begin
if (peak_dbin_mag[b2_rb] > 0) if (peak_dbin_mag[b2_rb] > 0)
nontrivial_count = nontrivial_count + 1; nontrivial_count = nontrivial_count + 1;
if (peak_dbin_mag[b2_rb] < min_peak) if (peak_dbin_mag[b2_rb] < min_peak)
@@ -727,10 +727,10 @@ initial begin
if (peak_dbin_mag[b2_rb] > max_peak) if (peak_dbin_mag[b2_rb] > max_peak)
max_peak = peak_dbin_mag[b2_rb]; max_peak = peak_dbin_mag[b2_rb];
end end
$display(" Doppler peak mag: min=%0d max=%0d, non-trivial in %0d/64 range bins", $display(" Doppler peak mag: min=%0d max=%0d, non-trivial in %0d/512 range bins",
min_peak, max_peak, nontrivial_count); min_peak, max_peak, nontrivial_count);
// All 64 range bins must have non-zero peak Doppler energy // All 512 range bins must have non-zero peak Doppler energy
check(nontrivial_count == 64, check(nontrivial_count == 512,
"B2a: All range bins have non-trivial Doppler energy"); "B2a: All range bins have non-trivial Doppler energy");
// Peak magnitude should be bounded (not overflowing to max signed value) // Peak magnitude should be bounded (not overflowing to max signed value)
check(max_peak < 32000, check(max_peak < 32000,
@@ -738,23 +738,23 @@ initial begin
end end
// ---- B3: Exact Doppler Output Count ---- // ---- B3: Exact Doppler Output Count ----
$display(" Doppler output count: %0d (expected 2048)", doppler_output_count); $display(" Doppler output count: %0d (expected 16384)", doppler_output_count);
check(doppler_output_count == 2048, check(doppler_output_count == 16384,
"B3: Exact output count = 2048 (64 range x 32 Doppler)"); "B3: Exact output count = 16384 (512 range x 32 Doppler)");
// ---- B4: Full Range/Doppler Bin Coverage (exact) ---- // ---- B4: Full Range/Doppler Bin Coverage (exact) ----
begin : b4_check_block begin : b4_check_block
integer b4_rb_count, b4_db_count, b4_i; integer b4_rb_count, b4_db_count, b4_i;
b4_rb_count = 0; b4_rb_count = 0;
b4_db_count = 0; b4_db_count = 0;
for (b4_i = 0; b4_i < 64; b4_i = b4_i + 1) begin for (b4_i = 0; b4_i < 512; b4_i = b4_i + 1) begin
if (range_bin_seen[b4_i]) b4_rb_count = b4_rb_count + 1; if (range_bin_seen[b4_i]) b4_rb_count = b4_rb_count + 1;
end end
for (b4_i = 0; b4_i < 32; b4_i = b4_i + 1) begin for (b4_i = 0; b4_i < 32; b4_i = b4_i + 1) begin
if (doppler_bin_seen[b4_i]) b4_db_count = b4_db_count + 1; if (doppler_bin_seen[b4_i]) b4_db_count = b4_db_count + 1;
end end
check(b4_rb_count == 64 && b4_db_count == 32, check(b4_rb_count == 512 && b4_db_count == 32,
"B4: Full bin coverage: 64 range x 32 Doppler"); "B4: Full bin coverage: 512 range x 32 Doppler");
end end
// ---- B5: No Duplicate Indices ---- // ---- B5: No Duplicate Indices ----
+6 -2
View File
@@ -1097,8 +1097,12 @@ initial begin
// ================================================================ // ================================================================
$display("--- Group 12: Watchdog / Liveness ---"); $display("--- Group 12: Watchdog / Liveness ---");
// G12.1: System hasn't hung — we reached this point // G12.1: System hasn't hung AND prior groups actually ran assertions.
check(1, "G12.1: System did not hang (reached final test group)"); // `check(1, ...)` alone is tautological; we additionally require that a
// meaningful number of earlier checks executed (catches a TB that skips
// past test groups via an @(posedge) that never fires or similar stall).
check(test_num > 20,
"G12.1: System reached final group AND >20 prior checks executed");
// G12.2: Total simulation time is within budget // G12.2: Total simulation time is within budget
check($time < SIM_TIMEOUT_NS, check($time < SIM_TIMEOUT_NS,