Integrate MTI canceller and DC notch filter for ground clutter removal

MTI canceller (2-pulse, H(z)=1-z^{-1}) between range decimator and
Doppler processor. Subtracts previous chirp from current, nulling DC
Doppler (stationary clutter). Pass-through when host_mti_enable=0.

DC notch filter (post-Doppler, pre-CFAR) zeros bins within
+/-host_dc_notch_width of DC. Complements MTI for residual clutter.

New host registers: 0x26 (mti_enable), 0x27 (dc_notch_width).
Both default to 0 (disabled) - fully backward-compatible.

Verification: 23/23 regression, 29/29 MTI standalone, 3/3 real-data
co-sim (5137/5137 exact match) all PASS.
This commit is contained in:
Jason
2026-03-20 16:39:17 +02:00
parent 075ae1e77a
commit ed629e7559
5 changed files with 751 additions and 14 deletions
+174
View File
@@ -0,0 +1,174 @@
`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)
);
// ============================================================================
// 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];
// ============================================================================
// 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;
end else begin
// 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
+38 -4
View File
@@ -51,7 +51,11 @@ module radar_receiver_final (
input wire stm32_new_azimuth_rx, input wire stm32_new_azimuth_rx,
// CFAR integration: expose Doppler frame_complete to top level // CFAR integration: expose Doppler frame_complete to top level
output wire doppler_frame_done_out output wire doppler_frame_done_out,
// Ground clutter removal controls
input wire host_mti_enable, // 1=MTI active, 0=pass-through
input wire [2:0] host_dc_notch_width // DC notch: zero Doppler bins within ±width of DC
); );
// ========== INTERNAL SIGNALS ========== // ========== INTERNAL SIGNALS ==========
@@ -102,6 +106,13 @@ wire signed [15:0] decimated_range_q;
wire decimated_range_valid; wire decimated_range_valid;
wire [5:0] decimated_range_bin; wire [5:0] decimated_range_bin;
// ========== MTI CANCELLER SIGNALS ==========
wire signed [15:0] mti_range_i;
wire signed [15:0] mti_range_q;
wire mti_range_valid;
wire [5:0] mti_range_bin;
wire mti_first_chirp;
// ========== RADAR MODE CONTROLLER SIGNALS ========== // ========== RADAR MODE CONTROLLER SIGNALS ==========
wire rmc_scanning; wire rmc_scanning;
wire rmc_scan_complete; wire rmc_scan_complete;
@@ -191,7 +202,7 @@ ddc_400m_enhanced ddc(
.baseband_valid_i(ddc_valid_i), // Valid at 100MHz .baseband_valid_i(ddc_valid_i), // Valid at 100MHz
.baseband_valid_q(ddc_valid_q), .baseband_valid_q(ddc_valid_q),
.mixers_enable(1'b1) .mixers_enable(1'b1)
); );
ddc_input_interface ddc_if ( ddc_input_interface ddc_if (
.clk(clk), .clk(clk),
@@ -328,6 +339,28 @@ range_bin_decimator #(
.watchdog_timeout() // Diagnostic unconnected (monitored via ILA if needed) .watchdog_timeout() // Diagnostic unconnected (monitored via ILA if needed)
); );
// ========== MTI CANCELLER (Ground Clutter Removal) ==========
// 2-pulse canceller: subtracts previous chirp from current chirp.
// H(z) = 1 - z^{-1} null at DC Doppler, removes stationary clutter.
// When host_mti_enable=0: transparent pass-through.
mti_canceller #(
.NUM_RANGE_BINS(64),
.DATA_WIDTH(16)
) mti_inst (
.clk(clk),
.reset_n(reset_n),
.range_i_in(decimated_range_i),
.range_q_in(decimated_range_q),
.range_valid_in(decimated_range_valid),
.range_bin_in(decimated_range_bin),
.range_i_out(mti_range_i),
.range_q_out(mti_range_q),
.range_valid_out(mti_range_valid),
.range_bin_out(mti_range_bin),
.mti_enable(host_mti_enable),
.mti_first_chirp(mti_first_chirp)
);
// ========== FRAME SYNC USING chirp_counter ========== // ========== FRAME SYNC USING chirp_counter ==========
reg [5:0] chirp_counter_prev; reg [5:0] chirp_counter_prev;
reg new_frame_pulse; reg new_frame_pulse;
@@ -360,8 +393,9 @@ end
assign new_chirp_frame = new_frame_pulse; assign new_chirp_frame = new_frame_pulse;
// ========== DATA PACKING FOR DOPPLER ========== // ========== DATA PACKING FOR DOPPLER ==========
assign range_data_32bit = {decimated_range_q, decimated_range_i}; // Use MTI-filtered data (or pass-through if MTI disabled)
assign range_data_valid = decimated_range_valid; assign range_data_32bit = {mti_range_q, mti_range_i};
assign range_data_valid = mti_range_valid;
// ========== DOPPLER PROCESSOR ========== // ========== DOPPLER PROCESSOR ==========
doppler_processor_optimized #( doppler_processor_optimized #(
+39 -6
View File
@@ -234,6 +234,10 @@ reg [7:0] host_cfar_alpha; // Opcode 0x23: threshold multiplier (Q4.4)
reg [1:0] host_cfar_mode; // Opcode 0x24: 00=CA, 01=GO, 10=SO reg [1:0] host_cfar_mode; // Opcode 0x24: 00=CA, 01=GO, 10=SO
reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold reg host_cfar_enable; // Opcode 0x25: 1=CFAR, 0=simple threshold
// Ground clutter removal registers (host-configurable via USB)
reg host_mti_enable; // Opcode 0x26: 1=MTI active, 0=pass-through
reg [2:0] host_dc_notch_width; // Opcode 0x27: DC notch ±width bins (0=off, 1..7)
// ============================================================================ // ============================================================================
// CLOCK BUFFERING // CLOCK BUFFERING
// ============================================================================ // ============================================================================
@@ -486,7 +490,10 @@ radar_receiver_final rx_inst (
.stm32_new_elevation_rx(stm32_new_elevation), .stm32_new_elevation_rx(stm32_new_elevation),
.stm32_new_azimuth_rx(stm32_new_azimuth), .stm32_new_azimuth_rx(stm32_new_azimuth),
// CFAR: Doppler frame-complete pulse // CFAR: Doppler frame-complete pulse
.doppler_frame_done_out(rx_frame_complete) .doppler_frame_done_out(rx_frame_complete),
// Ground clutter removal
.host_mti_enable(host_mti_enable),
.host_dc_notch_width(host_dc_notch_width)
); );
// ============================================================================ // ============================================================================
@@ -499,6 +506,26 @@ assign rx_doppler_real = rx_doppler_output[15:0];
assign rx_doppler_imag = rx_doppler_output[31:16]; assign rx_doppler_imag = rx_doppler_output[31:16];
assign rx_doppler_data_valid = rx_doppler_valid; assign rx_doppler_data_valid = rx_doppler_valid;
// ============================================================================
// DC NOTCH FILTER (post-Doppler-FFT, pre-CFAR)
// ============================================================================
// Zeros out Doppler bins within ±host_dc_notch_width of DC (bin 0).
// In a 32-point FFT, DC is bin 0; negative Doppler wraps to bins 31,30,...
// notch_width=1 → zero bins {0}. notch_width=2 → zero bins {0,1,31}. etc.
// When host_dc_notch_width=0: pass-through (no zeroing).
wire dc_notch_active;
wire [4:0] dop_bin_unsigned = rx_doppler_bin;
assign dc_notch_active = (host_dc_notch_width != 3'd0) &&
(dop_bin_unsigned < {2'b0, host_dc_notch_width} ||
dop_bin_unsigned > (5'd31 - {2'b0, host_dc_notch_width} + 5'd1));
// Notched Doppler data: zero I/Q when in notch zone, pass through otherwise
wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output;
wire notched_doppler_valid = rx_doppler_valid;
wire [4:0] notched_doppler_bin = rx_doppler_bin;
wire [5:0] notched_range_bin = rx_range_bin;
// ============================================================================ // ============================================================================
// CFAR DETECTOR (replaces simple threshold detector) // CFAR DETECTOR (replaces simple threshold detector)
// ============================================================================ // ============================================================================
@@ -520,11 +547,11 @@ cfar_ca cfar_inst (
.clk(clk_100m_buf), .clk(clk_100m_buf),
.reset_n(sys_reset_n), .reset_n(sys_reset_n),
// Doppler processor outputs // Doppler processor outputs (DC-notch filtered)
.doppler_data(rx_doppler_output), .doppler_data(notched_doppler_data),
.doppler_valid(rx_doppler_valid), .doppler_valid(notched_doppler_valid),
.doppler_bin_in(rx_doppler_bin), .doppler_bin_in(notched_doppler_bin),
.range_bin_in(rx_range_bin), .range_bin_in(notched_range_bin),
.frame_complete(rx_frame_complete), .frame_complete(rx_frame_complete),
// Configuration // Configuration
@@ -703,6 +730,9 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
host_cfar_alpha <= 8'h30; // alpha=3.0 (Q4.4) host_cfar_alpha <= 8'h30; // alpha=3.0 (Q4.4)
host_cfar_mode <= 2'b00; // CA-CFAR host_cfar_mode <= 2'b00; // CA-CFAR
host_cfar_enable <= 1'b0; // Disabled (simple threshold) host_cfar_enable <= 1'b0; // Disabled (simple threshold)
// Ground clutter removal defaults (disabled backward-compatible)
host_mti_enable <= 1'b0; // MTI off
host_dc_notch_width <= 3'd0; // DC notch off
end else begin end else begin
host_trigger_pulse <= 1'b0; // Self-clearing pulse host_trigger_pulse <= 1'b0; // Self-clearing pulse
host_status_request <= 1'b0; // Self-clearing pulse host_status_request <= 1'b0; // Self-clearing pulse
@@ -741,6 +771,9 @@ always @(posedge clk_100m_buf or negedge sys_reset_n) begin
8'h23: host_cfar_alpha <= usb_cmd_value[7:0]; 8'h23: host_cfar_alpha <= usb_cmd_value[7:0];
8'h24: host_cfar_mode <= usb_cmd_value[1:0]; 8'h24: host_cfar_mode <= usb_cmd_value[1:0];
8'h25: host_cfar_enable <= usb_cmd_value[0]; 8'h25: host_cfar_enable <= usb_cmd_value[0];
// Ground clutter removal opcodes
8'h26: host_mti_enable <= usb_cmd_value[0];
8'h27: host_dc_notch_width <= usb_cmd_value[2:0];
8'hFF: host_status_request <= 1'b1; // Gap 2: status readback 8'hFF: host_status_request <= 1'b1; // Gap 2: status readback
default: ; default: ;
endcase endcase
+9 -4
View File
@@ -74,6 +74,7 @@ PROD_RTL=(
radar_mode_controller.v radar_mode_controller.v
rx_gain_control.v rx_gain_control.v
cfar_ca.v cfar_ca.v
mti_canceller.v
) )
# Source-only RTL (not instantiated at top level, but should still be lint-clean) # Source-only RTL (not instantiated at top level, but should still be lint-clean)
@@ -377,6 +378,10 @@ run_test "RX Gain Control (digital gain)" \
tb/tb_rx_gain_control.vvp \ tb/tb_rx_gain_control.vvp \
tb/tb_rx_gain_control.v rx_gain_control.v tb/tb_rx_gain_control.v rx_gain_control.v
run_test "MTI Canceller (ground clutter)" \
tb/tb_mti_canceller.vvp \
tb/tb_mti_canceller.v mti_canceller.v
run_test "CFAR CA Detector" \ run_test "CFAR CA Detector" \
tb/tb_cfar_ca.vvp \ tb/tb_cfar_ca.vvp \
tb/tb_cfar_ca.v cfar_ca.v tb/tb_cfar_ca.v cfar_ca.v
@@ -405,7 +410,7 @@ if [[ "$QUICK" -eq 0 ]]; then
chirp_memory_loader_param.v latency_buffer.v \ chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
rx_gain_control.v rx_gain_control.v mti_canceller.v
# Golden compare # Golden compare
run_test "Receiver (golden compare)" \ run_test "Receiver (golden compare)" \
@@ -417,7 +422,7 @@ if [[ "$QUICK" -eq 0 ]]; then
chirp_memory_loader_param.v latency_buffer.v \ chirp_memory_loader_param.v latency_buffer.v \
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
rx_gain_control.v rx_gain_control.v mti_canceller.v
# Full system top (monitoring-only, legacy) # Full system top (monitoring-only, legacy)
run_test "System Top (radar_system_tb)" \ run_test "System Top (radar_system_tb)" \
@@ -431,7 +436,7 @@ if [[ "$QUICK" -eq 0 ]]; then
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v \ usb_data_interface.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v rx_gain_control.v cfar_ca.v mti_canceller.v
# E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset) # E2E integration (46 strict checks: TX, RX, USB R/W, CDC, safety, reset)
run_test "System E2E (tb_system_e2e)" \ run_test "System E2E (tb_system_e2e)" \
@@ -445,7 +450,7 @@ if [[ "$QUICK" -eq 0 ]]; then
matched_filter_multi_segment.v matched_filter_processing_chain.v \ matched_filter_multi_segment.v matched_filter_processing_chain.v \
range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.v \
usb_data_interface.v edge_detector.v radar_mode_controller.v \ usb_data_interface.v edge_detector.v radar_mode_controller.v \
rx_gain_control.v cfar_ca.v rx_gain_control.v cfar_ca.v mti_canceller.v
else else
echo " (skipped receiver golden + system top + E2E — use without --quick)" echo " (skipped receiver golden + system top + E2E — use without --quick)"
SKIP=$((SKIP + 4)) SKIP=$((SKIP + 4))
+491
View File
@@ -0,0 +1,491 @@
`timescale 1ns / 1ps
/**
* tb_mti_canceller.v
*
* Testbench for mti_canceller.v (Moving Target Indication).
* Uses [PASS]/[FAIL] markers for run_regression.sh compatibility.
*
* Tests:
* T1: Pass-through mode (mti_enable=0) data unchanged
* T2: First chirp muted (zeros) when MTI enabled
* T3: Second chirp = current - previous (correct subtraction)
* T4: Stationary target cancels to zero
* T5: Moving target (phase shift) passes through
* T6: Saturation on large difference
* T7: Enable toggle mid-stream clean transition
* T8: Reset during operation clean recovery
* T9: range_bin_out tracks range_bin_in
* T10: Back-to-back chirps (3+ chirps, verify continuous operation)
* T11: Negative input values handled correctly
*/
module tb_mti_canceller;
parameter DATA_W = 16;
parameter NUM_BINS = 64;
parameter CLK_PERIOD = 10;
reg clk;
reg reset_n;
reg signed [DATA_W-1:0] range_i_in;
reg signed [DATA_W-1:0] range_q_in;
reg range_valid_in;
reg [5:0] range_bin_in;
reg mti_enable;
wire signed [DATA_W-1:0] range_i_out;
wire signed [DATA_W-1:0] range_q_out;
wire range_valid_out;
wire [5:0] range_bin_out;
wire mti_first_chirp;
integer pass_count, fail_count;
// Output capture
reg signed [DATA_W-1:0] cap_i [0:NUM_BINS-1];
reg signed [DATA_W-1:0] cap_q [0:NUM_BINS-1];
reg [5:0] cap_bin [0:NUM_BINS-1];
integer cap_count;
mti_canceller #(
.NUM_RANGE_BINS(NUM_BINS),
.DATA_WIDTH(DATA_W)
) dut (
.clk(clk),
.reset_n(reset_n),
.range_i_in(range_i_in),
.range_q_in(range_q_in),
.range_valid_in(range_valid_in),
.range_bin_in(range_bin_in),
.range_i_out(range_i_out),
.range_q_out(range_q_out),
.range_valid_out(range_valid_out),
.range_bin_out(range_bin_out),
.mti_enable(mti_enable),
.mti_first_chirp(mti_first_chirp)
);
initial clk = 0;
always #(CLK_PERIOD/2) clk = ~clk;
task check;
input integer tnum;
input [255:0] desc;
input condition;
begin
if (condition) begin
$display("[PASS(T%0d)] %0s", tnum, desc);
pass_count = pass_count + 1;
end else begin
$display("[FAIL(T%0d)] %0s", tnum, desc);
fail_count = fail_count + 1;
end
end
endtask
task do_reset;
begin
reset_n = 0;
range_i_in = 0;
range_q_in = 0;
range_valid_in = 0;
range_bin_in = 0;
repeat (5) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
end
endtask
// Feed one range bin sample
task feed_sample;
input [5:0] bin;
input signed [DATA_W-1:0] i_val;
input signed [DATA_W-1:0] q_val;
begin
@(posedge clk);
range_i_in <= i_val;
range_q_in <= q_val;
range_valid_in <= 1'b1;
range_bin_in <= bin;
@(posedge clk);
range_valid_in <= 1'b0;
end
endtask
// Feed a full chirp (64 range bins) with constant I/Q
task feed_chirp_const;
input signed [DATA_W-1:0] i_val;
input signed [DATA_W-1:0] q_val;
integer r;
begin
for (r = 0; r < NUM_BINS; r = r + 1) begin
feed_sample(r[5:0], i_val, q_val);
end
end
endtask
// Feed a chirp where bin r has value i_base + r*i_step
task feed_chirp_ramp;
input signed [DATA_W-1:0] i_base;
input signed [DATA_W-1:0] i_step;
input signed [DATA_W-1:0] q_val;
integer r;
begin
for (r = 0; r < NUM_BINS; r = r + 1) begin
feed_sample(r[5:0], i_base + i_step * r[DATA_W-1:0], q_val);
end
end
endtask
// Capture outputs during a chirp
task capture_chirp;
integer timeout;
begin
cap_count = 0;
timeout = NUM_BINS * 4 + 100;
while (cap_count < NUM_BINS && timeout > 0) begin
@(posedge clk);
timeout = timeout - 1;
if (range_valid_out) begin
cap_i[cap_count] = range_i_out;
cap_q[cap_count] = range_q_out;
cap_bin[cap_count] = range_bin_out;
cap_count = cap_count + 1;
end
end
end
endtask
integer i;
reg all_zero;
reg all_match;
reg signed [DATA_W-1:0] expected;
initial begin
$dumpfile("tb_mti_canceller.vcd");
$dumpvars(0, tb_mti_canceller);
pass_count = 0;
fail_count = 0;
// ================================================================
// T1: Pass-through mode
// ================================================================
do_reset;
mti_enable = 1'b0;
// Feed one chirp with known data, capture output
fork
feed_chirp_const(16'sd1000, 16'sd500);
capture_chirp;
join
check(1, "T1.1: Pass-through: 64 outputs", cap_count == 64);
check(1, "T1.2: Pass-through: I[0]=1000", cap_i[0] == 16'sd1000);
check(1, "T1.3: Pass-through: Q[0]=500", cap_q[0] == 16'sd500);
check(1, "T1.4: Pass-through: I[63]=1000", cap_i[63] == 16'sd1000);
// ================================================================
// T2: First chirp muted when MTI enabled
// ================================================================
do_reset;
mti_enable = 1'b1;
fork
feed_chirp_const(16'sd5000, 16'sd3000);
capture_chirp;
join
all_zero = 1;
for (i = 0; i < cap_count; i = i + 1) begin
if (cap_i[i] != 0 || cap_q[i] != 0) all_zero = 0;
end
check(2, "T2.1: First chirp: 64 outputs", cap_count == 64);
check(2, "T2.2: First chirp: all zeros (muted)", all_zero == 1);
check(2, "T2.3: First chirp: mti_first_chirp was high", dut.has_previous == 1);
// ================================================================
// T3: Second chirp = current - previous
// ================================================================
// Previous chirp had I=5000, Q=3000. New chirp: I=7000, Q=4000.
// Expected: I=2000, Q=1000.
fork
feed_chirp_const(16'sd7000, 16'sd4000);
capture_chirp;
join
check(3, "T3.1: Second chirp: 64 outputs", cap_count == 64);
check(3, "T3.2: MTI I[0] = 7000-5000 = 2000", cap_i[0] == 16'sd2000);
check(3, "T3.3: MTI Q[0] = 4000-3000 = 1000", cap_q[0] == 16'sd1000);
check(3, "T3.4: MTI I[32] = 2000", cap_i[32] == 16'sd2000);
// ================================================================
// T4: Stationary target cancels to zero
// ================================================================
// Feed identical chirp as previous (7000, 4000). Diff = 0.
fork
feed_chirp_const(16'sd7000, 16'sd4000);
capture_chirp;
join
all_zero = 1;
for (i = 0; i < cap_count; i = i + 1) begin
if (cap_i[i] != 0 || cap_q[i] != 0) all_zero = 0;
end
check(4, "T4: Stationary target cancels to zero", all_zero == 1);
// ================================================================
// T5: Moving target passes through
// ================================================================
// Previous was (7000, 4000). New chirp: some bins different, some same.
// Bin 10: I=10000 diff=3000. Bin 30: I=7000 diff=0. Rest same.
begin : t5_block
integer r;
cap_count = 0;
for (r = 0; r < NUM_BINS; r = r + 1) begin
if (r == 10)
feed_sample(r[5:0], 16'sd10000, 16'sd4000);
else if (r == 30)
feed_sample(r[5:0], 16'sd7000, 16'sd4000);
else
feed_sample(r[5:0], 16'sd7000, 16'sd4000);
end
// Wait for outputs
repeat (10) @(posedge clk);
end
// Re-capture: since we didn't fork/join, manually count
// Actually let me re-do this properly
do_reset;
mti_enable = 1'b1;
// Chirp 1 (stored, output muted)
fork
feed_chirp_const(16'sd7000, 16'sd4000);
capture_chirp;
join
// Chirp 2: bin 10 has moving target
begin : t5_feed
integer r;
for (r = 0; r < NUM_BINS; r = r + 1) begin
if (r == 10)
feed_sample(r[5:0], 16'sd10000, 16'sd6000);
else
feed_sample(r[5:0], 16'sd7000, 16'sd4000);
end
end
// Capture in parallel didn't work cleanly with named blocks, so just wait
repeat (5) @(posedge clk);
// Check: we need to capture during feed. Let me use a different approach.
// Since feed_sample takes 2 cycles and output comes 1 cycle after valid_in,
// outputs interleave with feeds. Let me just check DUT state.
// Actually the capture task expects outputs; the issue is fork/join with
// named blocks in iverilog. Let me restructure.
// Reset and redo T5 cleanly
do_reset;
mti_enable = 1'b1;
// Chirp 1: all constant
fork
feed_chirp_const(16'sd1000, 16'sd500);
capture_chirp;
join
// Chirp 2: bin 20 has a moving target (I=5000 vs previous 1000)
cap_count = 0;
fork
begin : t5_feed2
integer r;
for (r = 0; r < NUM_BINS; r = r + 1) begin
if (r == 20)
feed_sample(r[5:0], 16'sd5000, 16'sd500);
else
feed_sample(r[5:0], 16'sd1000, 16'sd500);
end
end
capture_chirp;
join
check(5, "T5.1: Moving target: 64 outputs", cap_count == 64);
check(5, "T5.2: Stationary bin 0: I=0", cap_i[0] == 16'sd0);
check(5, "T5.3: Moving bin 20: I=4000", cap_i[20] == 16'sd4000);
check(5, "T5.4: Moving bin 20: Q=0", cap_q[20] == 16'sd0);
check(5, "T5.5: Stationary bin 63: I=0", cap_i[63] == 16'sd0);
// ================================================================
// T6: Saturation
// ================================================================
do_reset;
mti_enable = 1'b1;
// Chirp 1: I = -32000
fork
feed_chirp_const(-16'sd32000, 16'sd0);
capture_chirp;
join
// Chirp 2: I = +32000. Diff = 64000, saturates to +32767.
cap_count = 0;
fork
feed_chirp_const(16'sd32000, 16'sd0);
capture_chirp;
join
check(6, "T6.1: Saturation: 64 outputs", cap_count == 64);
check(6, "T6.2: Saturated I = 32767", cap_i[0] == 16'sd32767);
// ================================================================
// T7: Enable toggle mid-stream
// ================================================================
do_reset;
mti_enable = 1'b0;
// Feed one chirp in pass-through
fork
feed_chirp_const(16'sd2000, 16'sd1000);
capture_chirp;
join
check(7, "T7.1: Pass-through I=2000", cap_i[0] == 16'sd2000);
// Enable MTI
mti_enable = 1'b1;
// First MTI chirp should be muted
cap_count = 0;
fork
feed_chirp_const(16'sd3000, 16'sd1500);
capture_chirp;
join
all_zero = 1;
for (i = 0; i < cap_count; i = i + 1) begin
if (cap_i[i] != 0 || cap_q[i] != 0) all_zero = 0;
end
check(7, "T7.2: After enable: first chirp muted", all_zero == 1);
// Second MTI chirp should subtract
cap_count = 0;
fork
feed_chirp_const(16'sd5000, 16'sd2500);
capture_chirp;
join
check(7, "T7.3: After enable: second chirp I=2000", cap_i[0] == 16'sd2000);
// ================================================================
// T8: Reset during operation
// ================================================================
do_reset;
mti_enable = 1'b1;
feed_chirp_const(16'sd1000, 16'sd500);
repeat (5) @(posedge clk);
// Reset mid-operation
reset_n = 0;
repeat (5) @(posedge clk);
reset_n = 1;
repeat (2) @(posedge clk);
check(8, "T8.1: After reset: first_chirp=1", mti_first_chirp == 1);
check(8, "T8.2: After reset: has_previous=0", dut.has_previous == 0);
// ================================================================
// T9: range_bin_out tracks range_bin_in
// ================================================================
do_reset;
mti_enable = 1'b0;
cap_count = 0;
fork
feed_chirp_const(16'sd100, 16'sd50);
capture_chirp;
join
all_match = 1;
for (i = 0; i < cap_count; i = i + 1) begin
if (cap_bin[i] != i[5:0]) all_match = 0;
end
check(9, "T9: range_bin_out matches range_bin_in for all 64 bins", all_match == 1);
// ================================================================
// T10: Three consecutive chirps
// ================================================================
do_reset;
mti_enable = 1'b1;
// Chirp 1 (muted)
fork
feed_chirp_const(16'sd1000, 16'sd0);
capture_chirp;
join
// Chirp 2: I=2000, diff=1000
cap_count = 0;
fork
feed_chirp_const(16'sd2000, 16'sd0);
capture_chirp;
join
check(10, "T10.1: Chirp 2: diff I=1000", cap_i[0] == 16'sd1000);
// Chirp 3: I=5000, diff=3000
cap_count = 0;
fork
feed_chirp_const(16'sd5000, 16'sd0);
capture_chirp;
join
check(10, "T10.2: Chirp 3: diff I=3000", cap_i[0] == 16'sd3000);
// ================================================================
// T11: Negative input values
// ================================================================
do_reset;
mti_enable = 1'b1;
// Chirp 1: I=-3000
fork
feed_chirp_const(-16'sd3000, -16'sd1000);
capture_chirp;
join
// Chirp 2: I=-1000. Diff = -1000 - (-3000) = 2000.
cap_count = 0;
fork
feed_chirp_const(-16'sd1000, -16'sd500);
capture_chirp;
join
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);
// ================================================================
// SUMMARY
// ================================================================
$display("");
$display("============================================");
$display(" MTI Canceller Testbench Results");
$display("============================================");
$display(" PASS: %0d", pass_count);
$display(" FAIL: %0d", fail_count);
$display("============================================");
if (fail_count > 0)
$display("[FAIL] %0d test(s) failed", fail_count);
else
$display("[PASS] All %0d tests passed", pass_count);
$finish;
end
initial begin
#10_000_000;
$display("[FAIL] Global watchdog timeout");
$finish;
end
endmodule