diff --git a/9_Firmware/9_2_FPGA/mti_canceller.v b/9_Firmware/9_2_FPGA/mti_canceller.v new file mode 100644 index 0000000..418a5ad --- /dev/null +++ b/9_Firmware/9_2_FPGA/mti_canceller.v @@ -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 diff --git a/9_Firmware/9_2_FPGA/radar_receiver_final.v b/9_Firmware/9_2_FPGA/radar_receiver_final.v index 544258e..17155ba 100644 --- a/9_Firmware/9_2_FPGA/radar_receiver_final.v +++ b/9_Firmware/9_2_FPGA/radar_receiver_final.v @@ -51,7 +51,11 @@ module radar_receiver_final ( input wire stm32_new_azimuth_rx, // 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 ========== @@ -102,6 +106,13 @@ wire signed [15:0] decimated_range_q; wire decimated_range_valid; 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 ========== wire rmc_scanning; wire rmc_scan_complete; @@ -191,7 +202,7 @@ ddc_400m_enhanced ddc( .baseband_valid_i(ddc_valid_i), // Valid at 100MHz .baseband_valid_q(ddc_valid_q), .mixers_enable(1'b1) -); +); ddc_input_interface ddc_if ( .clk(clk), @@ -328,6 +339,28 @@ range_bin_decimator #( .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 ========== reg [5:0] chirp_counter_prev; reg new_frame_pulse; @@ -360,8 +393,9 @@ end assign new_chirp_frame = new_frame_pulse; // ========== DATA PACKING FOR DOPPLER ========== -assign range_data_32bit = {decimated_range_q, decimated_range_i}; -assign range_data_valid = decimated_range_valid; +// Use MTI-filtered data (or pass-through if MTI disabled) +assign range_data_32bit = {mti_range_q, mti_range_i}; +assign range_data_valid = mti_range_valid; // ========== DOPPLER PROCESSOR ========== doppler_processor_optimized #( diff --git a/9_Firmware/9_2_FPGA/radar_system_top.v b/9_Firmware/9_2_FPGA/radar_system_top.v index f8826e8..89886b0 100644 --- a/9_Firmware/9_2_FPGA/radar_system_top.v +++ b/9_Firmware/9_2_FPGA/radar_system_top.v @@ -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 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 // ============================================================================ @@ -486,7 +490,10 @@ radar_receiver_final rx_inst ( .stm32_new_elevation_rx(stm32_new_elevation), .stm32_new_azimuth_rx(stm32_new_azimuth), // 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_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) // ============================================================================ @@ -520,11 +547,11 @@ cfar_ca cfar_inst ( .clk(clk_100m_buf), .reset_n(sys_reset_n), - // Doppler processor outputs - .doppler_data(rx_doppler_output), - .doppler_valid(rx_doppler_valid), - .doppler_bin_in(rx_doppler_bin), - .range_bin_in(rx_range_bin), + // Doppler processor outputs (DC-notch filtered) + .doppler_data(notched_doppler_data), + .doppler_valid(notched_doppler_valid), + .doppler_bin_in(notched_doppler_bin), + .range_bin_in(notched_range_bin), .frame_complete(rx_frame_complete), // 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_mode <= 2'b00; // CA-CFAR 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 host_trigger_pulse <= 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'h24: host_cfar_mode <= usb_cmd_value[1: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 default: ; endcase diff --git a/9_Firmware/9_2_FPGA/run_regression.sh b/9_Firmware/9_2_FPGA/run_regression.sh index fe1b749..dca532f 100755 --- a/9_Firmware/9_2_FPGA/run_regression.sh +++ b/9_Firmware/9_2_FPGA/run_regression.sh @@ -74,6 +74,7 @@ PROD_RTL=( radar_mode_controller.v rx_gain_control.v cfar_ca.v + mti_canceller.v ) # 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.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" \ tb/tb_cfar_ca.vvp \ 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 \ matched_filter_multi_segment.v matched_filter_processing_chain.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 run_test "Receiver (golden compare)" \ @@ -417,7 +422,7 @@ if [[ "$QUICK" -eq 0 ]]; then chirp_memory_loader_param.v latency_buffer.v \ matched_filter_multi_segment.v matched_filter_processing_chain.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) 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 \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.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) 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 \ range_bin_decimator.v doppler_processor.v xfft_32.v fft_engine.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 echo " (skipped receiver golden + system top + E2E — use without --quick)" SKIP=$((SKIP + 4)) diff --git a/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v b/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v new file mode 100644 index 0000000..fe6cc07 --- /dev/null +++ b/9_Firmware/9_2_FPGA/tb/tb_mti_canceller.v @@ -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