From 89dc9156c7a3c82749c18c037e0e2d666bf50c3f Mon Sep 17 00:00:00 2001 From: Jason <83615043+JJassonn69@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:58:08 +0545 Subject: [PATCH] =?UTF-8?q?fix(fpga):=20RX-F=20=E2=80=94=20MTI=20exits=20m?= =?UTF-8?q?ute=20on=20chirp=20boundary,=20not=20just=20last=20bin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- 9_Firmware/9_2_FPGA/mti_canceller.v | 106 +++++++++++++++------- 9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v | 55 +++++++++++ 2 files changed, 126 insertions(+), 35 deletions(-) diff --git a/9_Firmware/9_2_FPGA/mti_canceller.v b/9_Firmware/9_2_FPGA/mti_canceller.v index aa73039..c483fd5 100644 --- a/9_Firmware/9_2_FPGA/mti_canceller.v +++ b/9_Firmware/9_2_FPGA/mti_canceller.v @@ -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 diff --git a/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v b/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v index beed23c..dfd4060 100644 --- a/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v +++ b/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v @@ -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 // ================================================================