feat: 2048-pt FFT upgrade with decimation=4, 512 output bins, 6m spacing

Complete cross-layer upgrade from 1024-pt/64-bin to 2048-pt/512-bin FFT:

FPGA RTL (14+ modules):
- radar_params.vh: FFT_SIZE=2048, RANGE_BINS=512, 9-bit range, 6-bit stream
- fft_engine.v: 2048-pt FFT with XPM BRAM
- chirp_memory_loader_param.v: 2 segments x 2048 (was 4 x 1024)
- matched_filter_multi_segment.v: BRAM inference for overlap_cache, explicit ov_waddr
- mti_canceller.v: BRAM inference for prev_i/q arrays (was fabric FFs)
- doppler_processor.v: 16384-deep memory, 14-bit addressing
- cfar_ca.v: 512 rows, indentation fix
- radar_receiver_final.v: rising-edge detector for frame_complete, 11-bit sample_addr
- range_bin_decimator.v: 512 output bins
- usb_data_interface_ft2232h.v: bulk per-frame with Manhattan magnitude
- radar_mode_controller.v: XOR edge detector for toggle signals
- rx_gain_control.v: updated for new bin count

Python GUI + Protocol (8 files):
- radar_protocol.py: 512-bin bulk frame parser, LSB-first bitmap
- GUI_V65_Tk.py, v7/*.py: updated for 512 bins, 6m range resolution

Golden data + tests:
- All .hex/.csv/.npy golden references regenerated for 2048/512
- fft_twiddle_2048.mem added
- Deleted stale seg2/seg3 chirp mem files
- 9 new bulk frame cross-layer tests, deleted 6 stale per-sample tests
- Deleted stale tb_cross_layer_ft2232h.v and dead contract_parser functions
- Updated validate_mem_files.py for 2048/2-segment config

MCU: RadarSettings.cpp max_distance/map_size 1536->3072

All 4 CI jobs pass: 285 tests, 0 failures, 0 skips
This commit is contained in:
Jason
2026-04-16 17:27:55 +05:45
parent affa40a9d3
commit e9705e40b7
178 changed files with 687738 additions and 122880 deletions
+89 -36
View File
@@ -19,25 +19,33 @@
* 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.
* The previous chirp's 512 range bins are stored in BRAM (inferred via
* sync-only read/write always blocks NO async reset on memory arrays).
* 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).
* When mti_enable=0, the module is a transparent pass-through.
*
* Resources:
* - 2 BRAM18 (64 x 16-bit I + 64 x 16-bit Q) or distributed RAM
* - ~30 LUTs (subtract + mux)
* - ~40 FFs (pipeline + control)
* BRAM inference note:
* prev_i/prev_q arrays use dedicated sync-only always blocks for read
* and write. This ensures Vivado infers BRAM (RAMB18) instead of fabric
* FFs + mux trees. The registered read adds 1 cycle of latency, which
* is compensated by a pipeline stage on the input data path.
*
* Resources (target):
* - 2 BRAM18 (512 x 16-bit I + 512 x 16-bit Q)
* - ~30 LUTs (subtract + mux + saturation)
* - ~80 FFs (pipeline + control)
* - 0 DSP48
*
* Clock domain: clk (100 MHz)
*/
`include "radar_params.vh"
module mti_canceller #(
parameter NUM_RANGE_BINS = 64,
parameter DATA_WIDTH = 16
parameter NUM_RANGE_BINS = `RP_NUM_RANGE_BINS, // 512
parameter DATA_WIDTH = `RP_DATA_WIDTH // 16
) (
input wire clk,
input wire reset_n,
@@ -46,13 +54,13 @@ module mti_canceller #(
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,
input wire [`RP_RANGE_BIN_BITS-1:0] range_bin_in, // 9-bit
// ========== 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,
output reg [`RP_RANGE_BIN_BITS-1:0] range_bin_out, // 9-bit
// ========== CONFIGURATION ==========
input wire mti_enable, // 1=MTI active, 0=pass-through
@@ -62,30 +70,79 @@ module mti_canceller #(
);
// ============================================================================
// PREVIOUS CHIRP BUFFER (64 x 16-bit I, 64 x 16-bit Q)
// PREVIOUS CHIRP BUFFER (512 x 16-bit I, 512 x 16-bit Q)
// ============================================================================
// Small enough for distributed RAM on XC7A200T (64 entries).
// Using separate I/Q arrays for clean read/write.
// BRAM-inferred on XC7A50T/200T (512 entries, sync-only read/write).
// Using separate I/Q arrays for clean dual-port inference.
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];
(* ram_style = "block" *) reg signed [DATA_WIDTH-1:0] prev_i [0:NUM_RANGE_BINS-1];
(* ram_style = "block" *) reg signed [DATA_WIDTH-1:0] prev_q [0:NUM_RANGE_BINS-1];
// ============================================================================
// INPUT PIPELINE STAGE (1 cycle delay to match BRAM read latency)
// ============================================================================
// Declarations must precede the BRAM write block that references them.
reg signed [DATA_WIDTH-1:0] range_i_d1, range_q_d1;
reg range_valid_d1;
reg [`RP_RANGE_BIN_BITS-1:0] range_bin_d1;
reg mti_enable_d1;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
range_i_d1 <= {DATA_WIDTH{1'b0}};
range_q_d1 <= {DATA_WIDTH{1'b0}};
range_valid_d1 <= 1'b0;
range_bin_d1 <= {`RP_RANGE_BIN_BITS{1'b0}};
mti_enable_d1 <= 1'b0;
end else begin
range_i_d1 <= range_i_in;
range_q_d1 <= range_q_in;
range_valid_d1 <= range_valid_in;
range_bin_d1 <= range_bin_in;
mti_enable_d1 <= mti_enable;
end
end
// ============================================================================
// BRAM WRITE PORT (sync only — NO async reset for BRAM inference)
// ============================================================================
// Writes the current chirp sample into prev_i/prev_q for next chirp's
// subtraction. Uses the delayed (d1) signals so the write happens 1 cycle
// after the read address is presented, avoiding RAW hazards.
always @(posedge clk) begin
if (range_valid_d1) begin
prev_i[range_bin_d1] <= range_i_d1;
prev_q[range_bin_d1] <= range_q_d1;
end
end
// ============================================================================
// BRAM READ PORT (sync only — 1 cycle read latency)
// ============================================================================
// Address is always driven by range_bin_in (cycle 0). Read data appears
// on prev_i_rd / prev_q_rd at cycle 1, aligned with the d1 pipeline stage.
reg signed [DATA_WIDTH-1:0] prev_i_rd, prev_q_rd;
always @(posedge clk) begin
prev_i_rd <= prev_i[range_bin_in];
prev_q_rd <= prev_q[range_bin_in];
end
// Track whether we have valid previous data
reg has_previous;
// ============================================================================
// MTI PROCESSING
// MTI PROCESSING (operates on d1 pipeline stage + BRAM read data)
// ============================================================================
// 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}
wire signed [DATA_WIDTH:0] diff_i_full = {range_i_d1[DATA_WIDTH-1], range_i_d1}
- {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}
wire signed [DATA_WIDTH:0] diff_q_full = {range_q_d1[DATA_WIDTH-1], range_q_d1}
- {prev_q_rd[DATA_WIDTH-1], prev_q_rd};
// Saturate to DATA_WIDTH bits
@@ -105,32 +162,28 @@ assign diff_q_sat = (diff_q_full > $signed({{2{1'b0}}, {(DATA_WIDTH-1){1'b1}}}))
: diff_q_full[DATA_WIDTH-1:0];
// ============================================================================
// MAIN LOGIC
// MAIN OUTPUT LOGIC (operates on d1 pipeline stage)
// ============================================================================
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;
range_bin_out <= {`RP_RANGE_BIN_BITS{1'b0}};
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;
if (range_valid_d1) begin
// Output path range_bin is from the delayed pipeline
range_bin_out <= range_bin_d1;
// Output path
range_bin_out <= range_bin_in;
if (!mti_enable) begin
if (!mti_enable_d1) begin
// Pass-through mode: no MTI processing
range_i_out <= range_i_in;
range_q_out <= range_q_in;
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;
@@ -144,7 +197,7 @@ always @(posedge clk or negedge reset_n) begin
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
if (range_bin_d1 == NUM_RANGE_BINS - 1) begin
has_previous <= 1'b1;
mti_first_chirp <= 1'b0;
end