Files
PLFM_RADAR/9_Firmware/9_2_FPGA/mti_canceller.v
T
Jason 3f47d1ef71 fix(pre-bringup): resolve P0 + quick-win P1 findings from 2026-04-19 audit
Addresses findings from docs/DEVELOP_AUDIT_2026-04-19.md:

P0 source-level:
- F-4.3 ADAR1000_Manager::adarSetTxPhase now writes REG_LOAD_WORKING
  with LD_WRK_REGS_LDTX_OVERRIDE (0x02) instead of 0x01. Previous value
  toggled the LDRX latch on a TX-phase write, so host TX phase updates
  never reached the working registers.
- F-6.1 DDC mixer_saturation / filter_overflow / diagnostics were deleted
  at the receiver boundary. Now plumbed to new outputs on
  radar_receiver_final (ddc_overflow_any, ddc_saturation_count) and
  aggregated into gpio_dig5 in radar_system_top. Added mark_debug
  attributes for ILA visibility. Test/debug inputs tied low explicitly.
- F-0.8 adc_clk_mmcm.xdc set_clock_uncertainty: removed invalid -add
  flag (Vivado silently rejected it, applying zero guardband). Now uses
  absolute 0.150 ns which covers 53 ps jitter + ~100 ps PVT margin.

P1:
- F-4.2 adarSetBit / adarResetBit reject broadcast=ON — the RMW sampled
  a single device but wrote to all four, clobbering the other three's
  state.
- F-4.4 initializeSingleDevice returns false and leaves initialized=false
  when scratchpad verification fails; previously marked the device
  initialized anyway so downstream PA enable could drive a dead bus.
- F-6.2 FIR I/Q filter_overflow ports, previously unconnected, now OR'd
  into the module-level filter_overflow output.
- F-6.3 mti_canceller exposes 8-bit saturation counter. Saturation was
  previously invisible and produces spurious Doppler harmonics.

Verification:
- 27/27 iverilog testbenches pass
- 228/228 pytest pass (cross-layer contract + cosim)
- MCU unit tests 51/51 + 24/24 pass
- Remote Vivado 2025.2 build: bitstream writes; 400 MHz mixer pipeline
  now shows WNS -0.109 ns which MATCHES the audit's F-0.9 prediction
  that the design only closed because F-0.8's guardband was silently
  dropped. ft_clkout F-0.9 remains a show-stopper (requires MRCC pin
  move), tracked separately.

Not addressed in this PR (larger scope, follow-up tickets):
F-0.4, F-0.5, F-0.6, F-0.7, F-0.9, F-1.1, F-1.2, F-2.2, F-3.2, F-4.1,
F-4.7, F-6.4, F-6.5.
2026-04-20 13:48:36 +05:45

192 lines
7.6 KiB
Verilog

`timescale 1ns / 1ps
/**
* mti_canceller.v
*
* Moving Target Indication (MTI) — 2-pulse canceller for ground clutter removal.
*
* Sits between the range bin decimator and the Doppler processor in the
* AERIS-10 receiver chain. Subtracts the previous chirp's range profile
* from the current chirp's profile, implementing H(z) = 1 - z^{-1} in
* slow-time. This places a null at zero Doppler (DC), removing stationary
* ground clutter while passing moving targets through.
*
* Signal chain position:
* Range Bin Decimator → [MTI Canceller] → Doppler Processor
*
* Algorithm:
* For each range bin r (0..NUM_RANGE_BINS-1):
* mti_out_i[r] = current_i[r] - previous_i[r]
* mti_out_q[r] = current_q[r] - previous_q[r]
*
* The previous chirp's 64 range bins are stored in a small BRAM.
* On the very first chirp after reset (or enable), there is no previous
* data — output is zero (muted) for that first chirp.
*
* When mti_enable=0, the module is a transparent pass-through with zero
* latency penalty (data goes straight through combinationally registered).
*
* Resources:
* - 2 BRAM18 (64 x 16-bit I + 64 x 16-bit Q) or distributed RAM
* - ~30 LUTs (subtract + mux)
* - ~40 FFs (pipeline + control)
* - 0 DSP48
*
* Clock domain: clk (100 MHz)
*/
module mti_canceller #(
parameter NUM_RANGE_BINS = 64,
parameter DATA_WIDTH = 16
) (
input wire clk,
input wire reset_n,
// ========== INPUT (from range bin decimator) ==========
input wire signed [DATA_WIDTH-1:0] range_i_in,
input wire signed [DATA_WIDTH-1:0] range_q_in,
input wire range_valid_in,
input wire [5:0] range_bin_in,
// ========== OUTPUT (to Doppler processor) ==========
output reg signed [DATA_WIDTH-1:0] range_i_out,
output reg signed [DATA_WIDTH-1:0] range_q_out,
output reg range_valid_out,
output reg [5:0] range_bin_out,
// ========== CONFIGURATION ==========
input wire mti_enable, // 1=MTI active, 0=pass-through
// ========== STATUS ==========
output reg mti_first_chirp, // 1 during first chirp (output muted)
// Audit F-6.3: count of saturated samples since last reset. Saturation
// here produces spurious Doppler harmonics (phantom targets at ±fs/2)
// and was previously invisible to the MCU. Saturates at 0xFF.
output reg [7:0] mti_saturation_count
);
// ============================================================================
// PREVIOUS CHIRP BUFFER (64 x 16-bit I, 64 x 16-bit Q)
// ============================================================================
// Small enough for distributed RAM on XC7A200T (64 entries).
// Using separate I/Q arrays for clean read/write.
reg signed [DATA_WIDTH-1:0] prev_i [0:NUM_RANGE_BINS-1];
reg signed [DATA_WIDTH-1:0] prev_q [0:NUM_RANGE_BINS-1];
// Track whether we have valid previous data
reg has_previous;
// ============================================================================
// MTI PROCESSING
// ============================================================================
// Read previous chirp data (combinational)
wire signed [DATA_WIDTH-1:0] prev_i_rd = prev_i[range_bin_in];
wire signed [DATA_WIDTH-1:0] prev_q_rd = prev_q[range_bin_in];
// Compute difference with saturation
// Subtraction can produce DATA_WIDTH+1 bits; saturate back to DATA_WIDTH.
wire signed [DATA_WIDTH:0] diff_i_full = {range_i_in[DATA_WIDTH-1], range_i_in}
- {prev_i_rd[DATA_WIDTH-1], prev_i_rd};
wire signed [DATA_WIDTH:0] diff_q_full = {range_q_in[DATA_WIDTH-1], range_q_in}
- {prev_q_rd[DATA_WIDTH-1], prev_q_rd};
// Saturate to DATA_WIDTH bits
wire signed [DATA_WIDTH-1:0] diff_i_sat;
wire signed [DATA_WIDTH-1:0] diff_q_sat;
assign diff_i_sat = (diff_i_full > $signed({{2{1'b0}}, {(DATA_WIDTH-1){1'b1}}}))
? $signed({1'b0, {(DATA_WIDTH-1){1'b1}}}) // +max
: (diff_i_full < $signed({{2{1'b1}}, {(DATA_WIDTH-1){1'b0}}}))
? $signed({1'b1, {(DATA_WIDTH-1){1'b0}}}) // -max
: diff_i_full[DATA_WIDTH-1:0];
assign diff_q_sat = (diff_q_full > $signed({{2{1'b0}}, {(DATA_WIDTH-1){1'b1}}}))
? $signed({1'b0, {(DATA_WIDTH-1){1'b1}}})
: (diff_q_full < $signed({{2{1'b1}}, {(DATA_WIDTH-1){1'b0}}}))
? $signed({1'b1, {(DATA_WIDTH-1){1'b0}}})
: diff_q_full[DATA_WIDTH-1:0];
// Saturation detection (F-6.3): the top two bits of the DATA_WIDTH+1 signed
// difference disagree iff the value exceeds the DATA_WIDTH signed range.
wire diff_i_overflow = (diff_i_full[DATA_WIDTH] != diff_i_full[DATA_WIDTH-1]);
wire diff_q_overflow = (diff_q_full[DATA_WIDTH] != diff_q_full[DATA_WIDTH-1]);
// ============================================================================
// MAIN LOGIC
// ============================================================================
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 <= 6'd0;
has_previous <= 1'b0;
mti_first_chirp <= 1'b1;
mti_saturation_count <= 8'd0;
end else begin
// Count saturated MTI-active samples (F-6.3). Clamp at 0xFF.
if (range_valid_in && mti_enable && has_previous
&& (diff_i_overflow || diff_q_overflow)
&& (mti_saturation_count != 8'hFF)) begin
mti_saturation_count <= mti_saturation_count + 8'd1;
end
// Default: no valid output
range_valid_out <= 1'b0;
if (range_valid_in) begin
// Always store current sample as "previous" for next chirp
prev_i[range_bin_in] <= range_i_in;
prev_q[range_bin_in] <= range_q_in;
// Output path
range_bin_out <= range_bin_in;
if (!mti_enable) begin
// Pass-through mode: no MTI processing
range_i_out <= range_i_in;
range_q_out <= range_q_in;
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) begin
// First chirp after enable: mute output (no subtraction possible).
// Still emit valid=1 with zero data so Doppler processor gets
// the expected number of samples per frame.
range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b1;
// After last range bin of first chirp, mark previous as valid
if (range_bin_in == NUM_RANGE_BINS - 1) begin
has_previous <= 1'b1;
mti_first_chirp <= 1'b0;
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;
end
end
end
end
// ============================================================================
// MEMORY INITIALIZATION (simulation only)
// ============================================================================
`ifdef SIMULATION
integer init_k;
initial begin
for (init_k = 0; init_k < NUM_RANGE_BINS; init_k = init_k + 1) begin
prev_i[init_k] = 0;
prev_q[init_k] = 0;
end
end
`endif
endmodule