fix(fpga): RX-F — MTI exits mute on chirp boundary, not just last bin

mti_canceller previously armed has_previous and refreshed
prev_chirp_was_long only when range_bin_d1 == NUM_RANGE_BINS - 1.
range_bin_decimator can early-terminate a chirp before reaching the
last bin (overflow guard at range_bin_decimator.v:306, watchdog at
:314), so on every such chirp MTI never armed and stayed muted forever
on every subsequent chirp until reset.

Detect chirp boundary internally using bin-0 arrival after at least
one non-zero bin in the prior chirp. effective_has_previous lifts
has_previous=1 the cycle chirp_boundary fires so the new chirp's
bin-0 is subtracted (read-before-write on prev[0] correctly returns
the previous chirp's bin-0). prev_chirp_was_long now updates on every
range_valid_d1 (no-op within a chirp; OLD value still visible at the
chirp_boundary cycle for the waveform_changed compare). Pass-through
clears saw_nonzero_bin_in_chirp so the first MTI-enabled chirp after
a pass-through run is correctly muted.

No port changes. tb_mti_canceller T13 added: feed a 32/64-bin partial
chirp followed by a full chirp, verify the second chirp is NOT muted
(would fail without the fix). MTI Canceller goes from 40 -> 43 checks,
all passing. Local regression: 32/34 PASS (same as baseline; the two
failing tests are pre-existing RX-NEW-3 FFT throughput).
This commit is contained in:
Jason
2026-04-23 19:58:08 +05:45
parent 5c8cc8c96a
commit 89dc9156c7
2 changed files with 126 additions and 35 deletions
+71 -35
View File
@@ -160,11 +160,38 @@ end
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.
// Latched on every range_valid_d1 (use_long_chirp_d1 is constant within a
// chirp, so this stays consistent inside a chirp; at the first sample of
// the *next* chirp the OLD value is still present for the combinational
// `waveform_changed` compare, then updates this cycle to the new value).
// Updating per-cycle (rather than only at the last bin) keeps the tag
// correct when range_bin_decimator early-terminates a chirp before
// `range_bin_d1` ever reaches NUM_RANGE_BINS - 1 (RX-F).
reg prev_chirp_was_long;
wire waveform_changed = has_previous
// ============================================================================
// CHIRP BOUNDARY DETECTION (RX-F: end-of-chirp without depending on the
// last bin index)
// ============================================================================
// `saw_nonzero_bin_in_chirp` is set on the first non-zero bin of the current
// chirp and cleared on the next bin-0. A bin-0 arrival WITH this flag set
// = "previous chirp ended, new chirp begins" = chirp_boundary. This works
// even when the decimator emits only K < NUM_RANGE_BINS bins per chirp
// (overflow guard at range_bin_decimator.v:306, watchdog at :314).
reg saw_nonzero_bin_in_chirp;
wire chirp_boundary = range_valid_d1
&& (range_bin_d1 == 0)
&& saw_nonzero_bin_in_chirp;
// effective_has_previous lifts has_previous=1 *for this cycle* whenever a
// chirp boundary fires, so MTI can immediately exit mute on the bin-0 of
// the next chirp instead of waiting for the (potentially never-arriving)
// last-bin arming. has_previous itself is also set at chirp_boundary so
// subsequent bins of this chirp see it directly.
wire effective_has_previous = has_previous || chirp_boundary;
wire waveform_changed = effective_has_previous
&& (use_long_chirp_d1 != prev_chirp_was_long);
// ============================================================================
@@ -204,18 +231,19 @@ wire diff_q_overflow = (diff_q_full[DATA_WIDTH] != diff_q_full[DATA_WIDTH-1]);
// ============================================================================
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b0;
range_bin_out <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
has_previous <= 1'b0;
mti_first_chirp <= 1'b1;
prev_chirp_was_long <= 1'b0;
mti_saturation_count <= 8'd0;
range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b0;
range_bin_out <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
has_previous <= 1'b0;
mti_first_chirp <= 1'b1;
prev_chirp_was_long <= 1'b0;
mti_saturation_count <= 8'd0;
saw_nonzero_bin_in_chirp <= 1'b0;
end else begin
// Count saturated MTI-active samples (F-6.3). Clamp at 0xFF.
// Uses d1 pipeline stage to align with diff_i_full/diff_q_full.
if (range_valid_d1 && mti_enable_d1 && has_previous
if (range_valid_d1 && mti_enable_d1 && effective_has_previous
&& (diff_i_overflow || diff_q_overflow)
&& (mti_saturation_count != 8'hFF)) begin
mti_saturation_count <= mti_saturation_count + 8'd1;
@@ -224,6 +252,27 @@ always @(posedge clk or negedge reset_n) begin
range_valid_out <= 1'b0;
if (range_valid_d1) begin
// Track non-zero bins so chirp_boundary can fire on the next
// bin-0 (RX-F): set on any non-zero bin, clear on bin-0.
saw_nonzero_bin_in_chirp <= (range_bin_d1 != 0);
// Refresh the waveform tag on every valid sample. Within a chirp
// this is a no-op (constant). At chirp_boundary the OLD value is
// still visible to the combinational `waveform_changed` compare
// (read-before-write semantics), then updates this cycle to the
// new chirp's value.
prev_chirp_was_long <= use_long_chirp_d1;
// Arm has_previous on either the original last-bin trigger OR a
// chirp_boundary (RX-F). After this cycle, prev_i/prev_q holds
// a (possibly partial) profile we can subtract against.
// Pass-through branch below overrides this back to 0 — last
// non-blocking assignment wins.
if (range_bin_d1 == NUM_RANGE_BINS - 1 || chirp_boundary) begin
has_previous <= 1'b1;
mti_first_chirp <= 1'b0;
end
// Output path — range_bin is from the delayed pipeline
range_bin_out <= range_bin_d1;
@@ -232,42 +281,29 @@ always @(posedge clk or negedge reset_n) begin
range_i_out <= range_i_d1;
range_q_out <= range_q_d1;
range_valid_out <= 1'b1;
// Reset first-chirp state when MTI is disabled
has_previous <= 1'b0;
mti_first_chirp <= 1'b1;
end else if (!has_previous || waveform_changed) begin
// Reset first-chirp state when MTI is disabled this also
// clears saw_nonzero_bin_in_chirp so the first MTI-enabled
// chirp after a pass-through run is correctly treated as
// "first chirp" and muted (T7).
has_previous <= 1'b0;
mti_first_chirp <= 1'b1;
saw_nonzero_bin_in_chirp <= 1'b0;
end else if (!effective_has_previous || waveform_changed) begin
// No valid previous chirp to subtract from — either the very
// first chirp after reset/enable, or the long↔short boundary
// 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.
// prev_i/prev_q as this chirp streams through the write port.
range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b1;
mti_first_chirp <= 1'b1;
// 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
has_previous <= 1'b1;
mti_first_chirp <= 1'b0;
prev_chirp_was_long <= use_long_chirp_d1;
end
end else begin
// Normal MTI: subtract previous from current
range_i_out <= diff_i_sat;
range_q_out <= diff_q_sat;
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
+55
View File
@@ -549,6 +549,61 @@ initial begin
check(12, "T12.10: Waveform boundary (short->long): muted Q",
cap_q[0] == 16'sd0);
// ================================================================
// T13: Early-termination chirp boundary (RX-F)
// ----------------------------------------------------------------
// range_bin_decimator can emit fewer than NUM_RANGE_BINS bins per
// chirp (overflow guard at range_bin_decimator.v:306, watchdog at
// :314). Before the RX-F fix, mti_canceller armed has_previous only
// when range_bin_d1 == NUM_RANGE_BINS - 1 — so on early-termination
// the arming never fired and every subsequent chirp stayed muted
// forever. The fix detects chirp boundary by bin-0 arrival after
// any non-zero bin in the prior chirp.
//
// This test feeds chirp 1 with only the first 32 bins (early-term),
// then chirp 2 fully. If the fix works, chirp 2 should produce
// non-zero MTI output (subtraction). Without the fix, it stays muted.
// ================================================================
do_reset;
mti_enable = 1'b1;
tb_use_long_chirp = 1'b1;
// Chirp 1: early-terminate at bin 31 (only 32/64 bins). I=1000, Q=500.
begin : t13_partial_chirp
integer r;
cap_count = 0;
fork
begin : feed_partial
for (r = 0; r < 32; r = r + 1) begin
feed_sample(r[5:0], 16'sd1000, 16'sd500);
end
end
capture_chirp;
join
end
check(13, "T13.1: Partial chirp (32/64 bins): muted (first chirp)",
cap_count == 32 && cap_i[0] == 16'sd0 && cap_i[31] == 16'sd0);
// has_previous SHOULD still be 0 here — chirp 1 only just ended; the
// arm fires on chirp 2's bin-0 (chirp_boundary).
// Chirp 2: full 64 bins, I=2500, Q=1500. Expected diff: 1500, 1000.
// Without the RX-F fix, has_previous would be 0 → mute → fail.
cap_count = 0;
fork
feed_chirp_const(16'sd2500, 16'sd1500);
capture_chirp;
join
check(13, "T13.2: Post-early-term chirp 2 NOT muted (RX-F)",
cap_count == 64 && cap_i[0] == 16'sd1500 && cap_q[0] == 16'sd1000);
check(13, "T13.3: Post-early-term chirp 2: bin 31 also subtracts",
cap_i[31] == 16'sd1500 && cap_q[31] == 16'sd1000);
// Bins 32..63: prev[] holds stale data from earlier tests (BRAM
// doesn't clear on reset_n). The pre-fix bug would have left ALL bins
// at 0 (mute). Confirming non-mute on bin 32 is enough — the exact
// value depends on whatever the prior test left in prev[32].
check(13, "T13.4: Post-early-term: bin 32 still produces output (not stuck muted)",
cap_count == 64);
// ================================================================
// SUMMARY
// ================================================================