fix(radar): RX chain corrections, GUI bin alignment, MCU boot ordering

FPGA — RX chain
  matched_filter_multi_segment.v: drop the gratuitous /4 scaling on
    DDC sign-extended input (was ddc_i[17:2] + ddc_i[1]); use
    ddc_i[15:0] directly. fft_engine has INTERNAL_W=32 with
    saturating 16-bit output, so full 16-bit input is safe. Restores
    ~12 dB of MF input dynamic range.
  radar_receiver_final.v: remove latency_buffer (count-N-pulses-then-
    prime FIFO that left frame 1 with all-zero ref). Replaced with
    a single-FF alignment register on ref_i/ref_q that matches the
    1-FF stage multi_segment ST_PROCESSING uses on adc_data.
    Verified by tb/tb_rxb_fullchain_latency.v — autocorrelation peak
    at bin 0 with peak/mean ~88x.
  doppler_processor.v / mti_canceller.v / cfar_ca.v /
    range_bin_decimator.v / radar_receiver_final.v / radar_system_top.v
    / usb_data_interface_ft2232h.v: switch port and parameter widths
    from RP_NUM_RANGE_BINS / RP_RANGE_BIN_BITS (always 512 / 9-bit)
    to RP_MAX_OUTPUT_BINS / RP_RANGE_BIN_WIDTH_MAX (auto-scales:
    50T 512 / 9-bit, 200T 4096 / 12-bit). Unblocks 200T 20 km mode
    at the RX module boundary; USB wire-protocol extension still
    pending.
  radar_receiver_final.v: doppler_frame_done_prev reset value 0 -> 1
    to prevent false done pulse on cycle 1 when level signal is
    HIGH at reset.
  matched_filter_processing_chain.v: delete the broken `ifdef
    SIMULATION inline behavioural FFT (482 lines removed). It
    produced wrong-bin peaks and 100-1000x weak magnitudes. Chain
    now uses production fft_engine.v + frequency_matched_filter.v
    in both iverilog and Vivado. Iverilog tests are ~38x slower per
    chain pass but produce correct results. Misleading "OK with
    Xilinx IP" comments at three test sites updated since the FFT
    is in-house, not an IP placeholder.

FPGA — testbenches
  tb/tb_rxb_latency_measure.v (new): measures chain internal pipeline
    depth (~2057 cycles, chirp-agnostic).
  tb/tb_rxb_fullchain_latency.v (new): full-chain autocorrelation
    verification — drives ddc with the same chirp samples the loader
    serves as ref, finds peak position and peak/mean.
  tb/tb_matched_filter_processing_chain.v: wait timeouts bumped
    50000 -> 500000 cycles to accommodate production FFT pipeline.

MCU
  main.cpp checkSystemHealthStatus: latch system_emergency_state on
    the error_count > 10 path so the SAFE-MODE blink loop in main()
    actually engages (was bypassed because predicate was false).
  main.cpp: move FPGA reset BEFORE the if(PowerAmplifier) block so
    adar_tr_x is driven LOW (RX commanded externally) before PA Vdd
    reaches 22 V. Old reset block at the original location removed.
  main.cpp MX_GPIO_Init: add GPIO_PIN_12 (FPGA reset) to the
    explicit WritePin(LOW) list so the safe initial state is no
    longer implicit.
  main.cpp checkSystemHealth: rate-limit ADAR1000
    verifyDeviceCommunication (HAL_Delay 1ms x 4 devices = 4 ms
    blocking SPI burst per main-loop iteration) from every-loop to
    every 2 s. readTemperature stays per-loop so over-temp
    detection latency is unchanged.
  USBHandler.cpp processSettingsData: dispatch threshold bumped
    74 -> 82 (matches parser minimum); buffer drained after parse
    attempt (slide remaining bytes left) so a false END find no
    longer sticks the buffer until 256-byte overflow.

GUI
  radar_protocol.py: NUM_RANGE_BINS 64 -> 512 (matches FPGA
    RP_NUM_RANGE_BINS); NUM_CELLS 2048 -> 16384.
  radar_protocol.py _ingest_sample: honor FPGA frame_start bit for
    resync after a USB drop; capture range_profile[rbin] once per
    range bin at dbin == 0 (FPGA emits the same range_i/range_q for
    all 32 Doppler cells of a given range bin; previous accumulator
    inflated the profile 32x).
  v7/models.py RadarSettings: range_resolution 24 -> 6 m (matches
    c/(2*100MHz)*4); max_distance and coverage_radius 1536 -> 3072 m;
    map_size 2000 -> 4000.
  v7/models.py WaveformConfig: n_range_bins 64 -> 512, fft_size
    1024 -> 2048, decimation_factor 16 -> 4.
  GUI_V65_Tk.py: _RANGE_PER_BIN math and stale "~24 m / ~1536 m"
    comments updated.
  test_v7.py: assertion values updated to match new defaults.

Tests
  test_ddc_cosim_fuzz.py: remove unused os/tempfile imports, wrap
    three long lines for ruff E501 compliance.
This commit is contained in:
Jason
2026-04-23 05:56:52 +05:45
parent 27c9c22ad2
commit 9d1eb4b11c
19 changed files with 752 additions and 635 deletions
@@ -77,20 +77,24 @@ void USBHandler::processSettingsData(const uint8_t* data, uint32_t length) {
DIAG("USB", " settings buffer: +%lu bytes, total=%lu/%u", (unsigned long)bytes_to_copy, (unsigned long)buffer_index, MAX_BUFFER_SIZE); DIAG("USB", " settings buffer: +%lu bytes, total=%lu/%u", (unsigned long)bytes_to_copy, (unsigned long)buffer_index, MAX_BUFFER_SIZE);
// Check if we have a complete settings packet (contains "SET" and "END") // Check if we have a complete settings packet (contains "SET" and "END")
if (buffer_index >= 74) { // Minimum size for valid settings packet // Minimum valid packet is "SET" + 9 doubles + 1 uint32 + "END" = 82 bytes
// (matches RadarSettings::parseFromUSB length check).
if (buffer_index >= 82) {
// Look for "SET" at beginning and "END" somewhere in the packet // Look for "SET" at beginning and "END" somewhere in the packet
bool has_set = (memcmp(usb_buffer, "SET", 3) == 0); bool has_set = (memcmp(usb_buffer, "SET", 3) == 0);
bool has_end = false; bool has_end = false;
uint32_t packet_len = 0;
DIAG_BOOL("USB", " packet starts with SET", has_set); DIAG_BOOL("USB", " packet starts with SET", has_set);
for (uint32_t i = 3; i <= buffer_index - 3; i++) { for (uint32_t i = 3; i <= buffer_index - 3; i++) {
if (memcmp(usb_buffer + i, "END", 3) == 0) { if (memcmp(usb_buffer + i, "END", 3) == 0) {
has_end = true; has_end = true;
DIAG("USB", " END marker found at offset %lu, packet_len=%lu", (unsigned long)i, (unsigned long)(i + 3)); packet_len = i + 3;
DIAG("USB", " END marker found at offset %lu, packet_len=%lu", (unsigned long)i, (unsigned long)packet_len);
// Parse the complete packet up to "END" // Parse the complete packet up to "END"
if (has_set && current_settings.parseFromUSB(usb_buffer, i + 3)) { if (has_set && current_settings.parseFromUSB(usb_buffer, packet_len)) {
current_state = USBState::READY_FOR_DATA; current_state = USBState::READY_FOR_DATA;
DIAG("USB", " Settings parsed OK, state -> READY_FOR_DATA"); DIAG("USB", " Settings parsed OK, state -> READY_FOR_DATA");
} else { } else {
@@ -100,10 +104,18 @@ void USBHandler::processSettingsData(const uint8_t* data, uint32_t length) {
} }
} }
// If we didn't find a valid packet but buffer is full, reset // [MCU-N9 FIX] Drain the consumed packet bytes (or false-positive END)
if (buffer_index >= MAX_BUFFER_SIZE && !has_end) { // so a parse failure doesn't leave the buffer stuck on the same bytes
// until MAX_BUFFER_SIZE overflow. Slide any remaining bytes left.
if (has_end && packet_len > 0) {
uint32_t remaining = buffer_index - packet_len;
if (remaining > 0) {
memmove(usb_buffer, usb_buffer + packet_len, remaining);
}
buffer_index = remaining;
} else if (buffer_index >= MAX_BUFFER_SIZE) {
DIAG_WARN("USB", " Buffer full (%u) without END marker -- resetting", MAX_BUFFER_SIZE); DIAG_WARN("USB", " Buffer full (%u) without END marker -- resetting", MAX_BUFFER_SIZE);
buffer_index = 0; // Reset buffer to avoid overflow buffer_index = 0;
} }
} }
} }
@@ -685,12 +685,22 @@ SystemError_t checkSystemHealth(void) {
} }
// 3. Check ADAR1000 Communication and Temperature // 3. Check ADAR1000 Communication and Temperature
// [MCU-N7 FIX] verifyDeviceCommunication() writes the SCRATCHPAD register
// and HAL_Delay(1) per device. Across 4 devices that is >=4 ms of
// blocking SPI per main-loop iteration (chirp jitter source). Rate-limit
// the comm check to every 2 s (matches the clock-check pattern at line
// 658). readTemperature() is single-register SPI read with no HAL_Delay,
// so it stays per-loop to keep PA over-temperature detection responsive.
static uint32_t last_adar_comm_check = 0;
bool run_comm_check = (HAL_GetTick() - last_adar_comm_check > 2000);
for (int i = 0; i < 4; i++) { for (int i = 0; i < 4; i++) {
if (run_comm_check) {
if (!adarManager.verifyDeviceCommunication(i)) { if (!adarManager.verifyDeviceCommunication(i)) {
current_error = ERROR_ADAR1000_COMM; current_error = ERROR_ADAR1000_COMM;
DIAG_ERR("BF", "Health check: ADAR1000 #%d comm FAILED", i); DIAG_ERR("BF", "Health check: ADAR1000 #%d comm FAILED", i);
return current_error; return current_error;
} }
}
float temp = adarManager.readTemperature(i); float temp = adarManager.readTemperature(i);
if (temp > 85.0f) { if (temp > 85.0f) {
@@ -699,6 +709,9 @@ SystemError_t checkSystemHealth(void) {
return current_error; return current_error;
} }
} }
if (run_comm_check) {
last_adar_comm_check = HAL_GetTick();
}
// 4. Check IMU Communication // 4. Check IMU Communication
static uint32_t last_imu_check = 0; static uint32_t last_imu_check = 0;
@@ -949,10 +962,19 @@ bool checkSystemHealthStatus(void) {
DIAG_ERR("SYS", "checkSystemHealthStatus: error detected (code %d), calling handleSystemError()", error); DIAG_ERR("SYS", "checkSystemHealthStatus: error detected (code %d), calling handleSystemError()", error);
handleSystemError(error); handleSystemError(error);
// If we're in emergency state or too many errors, shutdown // If we're in emergency state or too many errors, shutdown.
// [MCU-N1 FIX] Latch system_emergency_state=true on the error_count>10
// path too — otherwise the SAFE-MODE blink loop in main() exits in one
// pass (its predicate is `while(system_emergency_state)`) and the main
// loop continues running with PA rails already cut by
// systemPowerDownSequence(), still toggling new_chirp via PD8.
if (system_emergency_state || error_count > 10) { if (system_emergency_state || error_count > 10) {
DIAG_ERR("SYS", "checkSystemHealthStatus returning FALSE (emergency=%s error_count=%lu)", if (!system_emergency_state) {
system_emergency_state ? "true" : "false", error_count); system_emergency_state = true;
DIAG_ERR("SYS", "Latching system_emergency_state due to error_count > 10");
}
DIAG_ERR("SYS", "checkSystemHealthStatus returning FALSE (emergency=true error_count=%lu)",
error_count);
return false; return false;
} }
} }
@@ -1834,6 +1856,24 @@ int main(void)
* the MCU at boot indefinitely. The USB settings handshake (if ever * the MCU at boot indefinitely. The USB settings handshake (if ever
* re-enabled) should be handled non-blocking in the main loop. */ * re-enabled) should be handled non-blocking in the main loop. */
/***************************************************************/
/************ FPGA reset (BEFORE PA Vdd enable) ****************/
/***************************************************************/
/* [MCU-N2/N11 FIX] Reset FPGA early — before any PA-rail enables —
* so `adar_tr_x` is driven LOW (RX commanded externally) when the PA Vdd
* rail later comes up to 22 V. Without this, PA could be energised while
* the FPGA is still in its implicit reset and `adar_tr_x` is undefined,
* with the ADAR1000 already commanded to TX (TR_SOURCE=1) — a glitch
* could key the PA into an undefined antenna load. Kept outside the
* `if (PowerAmplifier)` block so the FPGA always boots cleanly even when
* the PA path is disabled for bench testing. TX mixer enable (PD11) is
* still LOW (set by MX_GPIO_Init), so no chirps fire. */
DIAG("FPGA", "Resetting FPGA (GPIOD pin 12: LOW -> 10ms -> HIGH)");
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET);
HAL_Delay(10);
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
DIAG("FPGA", "FPGA reset complete -- adar_tr_x driven LOW (RX commanded)");
/***************************************************************/ /***************************************************************/
/************RF Power Amplifier Powering up sequence************/ /************RF Power Amplifier Powering up sequence************/
/***************************************************************/ /***************************************************************/
@@ -1891,6 +1931,10 @@ int main(void)
HAL_GPIO_WritePin(DAC_2_VG_LDAC_GPIO_Port, DAC_2_VG_LDAC_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(DAC_2_VG_LDAC_GPIO_Port, DAC_2_VG_LDAC_Pin, GPIO_PIN_SET);
//Enable RF Power Amplifier VDD = 22V //Enable RF Power Amplifier VDD = 22V
/* [MCU-N2/N11] FPGA has already been reset earlier (before this PA block)
* so `adar_tr_x` is now driven LOW (RX commanded). Safe to bring PA Vdd
* up to 22 V here. TX mixer enable (PD11) is still LOW until later,
* gating any FPGA-driven chirps. */
DIAG("PA", "Enabling RFPA VDD=22V (EN_DIS_RFPA_VDD HIGH)"); DIAG("PA", "Enabling RFPA VDD=22V (EN_DIS_RFPA_VDD HIGH)");
HAL_GPIO_WritePin(EN_DIS_RFPA_VDD_GPIO_Port, EN_DIS_RFPA_VDD_Pin, GPIO_PIN_SET); HAL_GPIO_WritePin(EN_DIS_RFPA_VDD_GPIO_Port, EN_DIS_RFPA_VDD_Pin, GPIO_PIN_SET);
@@ -1971,12 +2015,10 @@ int main(void)
DIAG("PA", "PA IDQ calibration sequence COMPLETE"); DIAG("PA", "PA IDQ calibration sequence COMPLETE");
} }
//RESET FPGA /* [MCU-N2/N11] FPGA was already reset earlier in the boot sequence,
DIAG("FPGA", "Resetting FPGA (GPIOD pin 12: LOW -> 10ms -> HIGH)"); * before PA Vdd was energised, to avoid an undefined `adar_tr_x` window.
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_RESET); * No further reset needed here. Leaving the comment so future readers
HAL_Delay(10); * understand why this block looks like it should be present. */
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_12, GPIO_PIN_SET);
DIAG("FPGA", "FPGA reset complete");
@@ -2730,7 +2772,7 @@ static void MX_GPIO_Init(void)
|EN_P_3V3_VDD_SW_Pin, GPIO_PIN_RESET); |EN_P_3V3_VDD_SW_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */ /*Configure GPIO pin Output Level */
HAL_GPIO_WritePin(GPIOD, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11 HAL_GPIO_WritePin(GPIOD, GPIO_PIN_8|GPIO_PIN_9|GPIO_PIN_10|GPIO_PIN_11|GPIO_PIN_12
|STEPPER_CW_P_Pin|STEPPER_CLK_P_Pin|EN_DIS_RFPA_VDD_Pin|EN_DIS_COOLING_Pin, GPIO_PIN_RESET); |STEPPER_CW_P_Pin|STEPPER_CLK_P_Pin|EN_DIS_RFPA_VDD_Pin|EN_DIS_COOLING_Pin, GPIO_PIN_RESET);
/*Configure GPIO pin Output Level */ /*Configure GPIO pin Output Level */
+9 -6
View File
@@ -61,8 +61,11 @@
`include "radar_params.vh" `include "radar_params.vh"
// [RX-D FIX] NUM_RANGE_BINS and range_bin port widths now scale with
// `RP_MAX_OUTPUT_BINS / `RP_RANGE_BIN_WIDTH_MAX (50T: 512/9, 200T: 4096/12).
// CFAR magnitude BRAM depth uses `RP_CFAR_MAG_DEPTH which already scales.
module cfar_ca #( module cfar_ca #(
parameter NUM_RANGE_BINS = `RP_NUM_RANGE_BINS, // 512 parameter NUM_RANGE_BINS = `RP_MAX_OUTPUT_BINS, // 512 (50T) / 4096 (200T)
parameter NUM_DOPPLER_BINS = `RP_NUM_DOPPLER_BINS, // 32 parameter NUM_DOPPLER_BINS = `RP_NUM_DOPPLER_BINS, // 32
parameter MAG_WIDTH = 17, parameter MAG_WIDTH = 17,
parameter ALPHA_WIDTH = 8, parameter ALPHA_WIDTH = 8,
@@ -76,7 +79,7 @@ module cfar_ca #(
input wire [31:0] doppler_data, input wire [31:0] doppler_data,
input wire doppler_valid, input wire doppler_valid,
input wire [4:0] doppler_bin_in, input wire [4:0] doppler_bin_in,
input wire [`RP_RANGE_BIN_BITS-1:0] range_bin_in, // 9-bit input wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_in, // 9-bit (50T) / 12-bit (200T)
input wire frame_complete, input wire frame_complete,
// ========== CONFIGURATION ========== // ========== CONFIGURATION ==========
@@ -90,7 +93,7 @@ module cfar_ca #(
// ========== DETECTION OUTPUTS ========== // ========== DETECTION OUTPUTS ==========
output reg detect_flag, output reg detect_flag,
output reg detect_valid, output reg detect_valid,
output reg [`RP_RANGE_BIN_BITS-1:0] detect_range, // 9-bit output reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] detect_range, // 9-bit (50T) / 12-bit (200T)
output reg [4:0] detect_doppler, output reg [4:0] detect_doppler,
output reg [MAG_WIDTH-1:0] detect_magnitude, output reg [MAG_WIDTH-1:0] detect_magnitude,
output reg [MAG_WIDTH-1:0] detect_threshold, output reg [MAG_WIDTH-1:0] detect_threshold,
@@ -105,10 +108,10 @@ module cfar_ca #(
// INTERNAL PARAMETERS // INTERNAL PARAMETERS
// ============================================================================ // ============================================================================
localparam TOTAL_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS; localparam TOTAL_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS;
localparam ADDR_WIDTH = `RP_CFAR_MAG_ADDR_W; // 14 localparam ADDR_WIDTH = `RP_CFAR_MAG_ADDR_W; // 14 (50T) / 17 (200T)
localparam COL_BITS = 5; localparam COL_BITS = 5;
localparam ROW_BITS = `RP_RANGE_BIN_BITS; // 9 localparam ROW_BITS = `RP_RANGE_BIN_WIDTH_MAX; // 9 (50T) / 12 (200T)
localparam SUM_WIDTH = MAG_WIDTH + ROW_BITS; // 26 bits: sum of up to 512 magnitudes localparam SUM_WIDTH = MAG_WIDTH + ROW_BITS; // 26 (50T) / 29 (200T)
localparam PROD_WIDTH = SUM_WIDTH + ALPHA_WIDTH; // 34 bits localparam PROD_WIDTH = SUM_WIDTH + ALPHA_WIDTH; // 34 bits
localparam ALPHA_FRAC_BITS = 4; // Q4.4 localparam ALPHA_FRAC_BITS = 4; // Q4.4
+13 -17
View File
@@ -35,21 +35,17 @@
`include "radar_params.vh" `include "radar_params.vh"
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// !!! 200T 20 km MODE BROKEN — FIX BEFORE 200T BRING-UP !!! // [RX-D FIX] RANGE_BINS and range_bin port now scale with `RP_MAX_OUTPUT_BINS
// RANGE_BINS and the range_bin output port default to `RP_NUM_RANGE_BINS // and `RP_RANGE_BIN_WIDTH_MAX (auto-conditional on SUPPORT_LONG_RANGE).
// (512) / `RP_RANGE_BIN_BITS (9). In 20 km mode the upstream pipeline // 50T (no SUPPORT_LONG_RANGE): 512 bins / 9-bit — 3 km only
// emits `RP_OUTPUT_RANGE_BINS_20KM = 4096 bins/chirp, which the internal // 200T (SUPPORT_LONG_RANGE): 4096 bins / 12-bit — 3 km and 20 km
// range-bin BRAMs and address counters here cannot represent — bins // In 3 km mode the upstream produces 512 bins (uses bins 0..511 only on 200T).
// 512..4095 alias onto bins 0..511 and the Doppler FFT collects a // In 20 km mode the upstream produces 4096 bins, which the BRAMs and counters
// scrambled slow-time vector per aliased range cell. // can now represent without aliasing.
// Latent on XC7A50T (SUPPORT_LONG_RANGE undefined → 3 km only); will
// corrupt all 20 km output on XC7A200T. Before 200T bring-up: scale
// RANGE_BINS with `RP_MAX_OUTPUT_BINS, widen range_bin, and resize the
// per-range chirp buffers, or route 20 km mode around this block.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
module doppler_processor_optimized #( module doppler_processor_optimized #(
parameter DOPPLER_FFT_SIZE = `RP_DOPPLER_FFT_SIZE, // 16 parameter DOPPLER_FFT_SIZE = `RP_DOPPLER_FFT_SIZE, // 16
parameter RANGE_BINS = `RP_NUM_RANGE_BINS, // 512 parameter RANGE_BINS = `RP_MAX_OUTPUT_BINS, // 512 (50T) / 4096 (200T)
parameter CHIRPS_PER_FRAME = `RP_CHIRPS_PER_FRAME, // 32 parameter CHIRPS_PER_FRAME = `RP_CHIRPS_PER_FRAME, // 32
parameter CHIRPS_PER_SUBFRAME = `RP_CHIRPS_PER_SUBFRAME, // 16 parameter CHIRPS_PER_SUBFRAME = `RP_CHIRPS_PER_SUBFRAME, // 16
parameter WINDOW_TYPE = 0, // 0=Hamming, 1=Rectangular parameter WINDOW_TYPE = 0, // 0=Hamming, 1=Rectangular
@@ -63,7 +59,7 @@ module doppler_processor_optimized #(
output reg [31:0] doppler_output, output reg [31:0] doppler_output,
output reg doppler_valid, output reg doppler_valid,
output reg [4:0] doppler_bin, // {sub_frame, bin[3:0]} output reg [4:0] doppler_bin, // {sub_frame, bin[3:0]}
output reg [`RP_RANGE_BIN_BITS-1:0] range_bin, // 9-bit output reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin, // 9-bit (50T) / 12-bit (200T)
output reg sub_frame, // 0=long PRI, 1=short PRI output reg sub_frame, // 0=long PRI, 1=short PRI
output wire processing_active, output wire processing_active,
output wire frame_complete, output wire frame_complete,
@@ -74,9 +70,9 @@ module doppler_processor_optimized #(
output wire [2:0] fv_state, output wire [2:0] fv_state,
output wire [`RP_DOPPLER_MEM_ADDR_W-1:0] fv_mem_write_addr, output wire [`RP_DOPPLER_MEM_ADDR_W-1:0] fv_mem_write_addr,
output wire [`RP_DOPPLER_MEM_ADDR_W-1:0] fv_mem_read_addr, output wire [`RP_DOPPLER_MEM_ADDR_W-1:0] fv_mem_read_addr,
output wire [`RP_RANGE_BIN_BITS-1:0] fv_write_range_bin, output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] fv_write_range_bin,
output wire [4:0] fv_write_chirp_index, output wire [4:0] fv_write_chirp_index,
output wire [`RP_RANGE_BIN_BITS-1:0] fv_read_range_bin, output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] fv_read_range_bin,
output wire [4:0] fv_read_doppler_index, output wire [4:0] fv_read_doppler_index,
output wire [9:0] fv_processing_timeout, output wire [9:0] fv_processing_timeout,
output wire fv_frame_buffer_full, output wire fv_frame_buffer_full,
@@ -130,9 +126,9 @@ localparam MEM_DEPTH = RANGE_BINS * CHIRPS_PER_FRAME;
// ============================================== // ==============================================
// Control Registers // Control Registers
// ============================================== // ==============================================
reg [`RP_RANGE_BIN_BITS-1:0] write_range_bin; reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] write_range_bin;
reg [4:0] write_chirp_index; reg [4:0] write_chirp_index;
reg [`RP_RANGE_BIN_BITS-1:0] read_range_bin; reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] read_range_bin;
reg [4:0] read_doppler_index; reg [4:0] read_doppler_index;
reg frame_buffer_full; reg frame_buffer_full;
reg [9:0] chirps_received; reg [9:0] chirps_received;
@@ -248,8 +248,13 @@ always @(posedge clk or negedge reset_n) begin
// Store in buffer via BRAM write port // Store in buffer via BRAM write port
buf_we <= 1; buf_we <= 1;
buf_waddr <= buffer_write_ptr[10:0]; buf_waddr <= buffer_write_ptr[10:0];
buf_wdata_i <= ddc_i[17:2] + ddc_i[1]; // [RX-A FIX] ddc_i = {{2{gc_i[15]}}, gc_i} top 2 bits are
buf_wdata_q <= ddc_q[17:2] + ddc_q[1]; // sign-extension. The previous `ddc_i[17:2] + ddc_i[1]`
// was a gratuitous /4 scaling (~12 dB dynamic-range loss).
// fft_engine has INTERNAL_W=32 with saturating 16-bit output,
// so full 16-bit input is safe (no bit-growth overflow risk).
buf_wdata_i <= ddc_i[15:0];
buf_wdata_q <= ddc_q[15:0];
buffer_write_ptr <= buffer_write_ptr + 1; buffer_write_ptr <= buffer_write_ptr + 1;
chirp_samples_collected <= chirp_samples_collected + 1; chirp_samples_collected <= chirp_samples_collected + 1;
@@ -6,10 +6,14 @@
* Pulse compression processing chain for AERIS-10 FMCW radar. * Pulse compression processing chain for AERIS-10 FMCW radar.
* Implements: FFT(signal) FFT(reference) Conjugate multiply IFFT * Implements: FFT(signal) FFT(reference) Conjugate multiply IFFT
* *
* This is a SIMULATION-COMPATIBLE implementation that replaces the Xilinx * Uses the in-house fft_engine.v (Radix-2 DIT, BRAM-backed) instantiated
* FFT IP cores (FFT_enhanced) with behavioral Radix-2 DIT FFT engines. * once and reused 3 times per frame, plus frequency_matched_filter.v for
* For synthesis, replace the behavioral FFT instances with the actual * the pipelined conjugate multiply. Same code path runs in iverilog
* Xilinx xfft IP blocks. * simulation and Vivado synthesis.
*
* (An earlier `ifdef SIMULATION inline behavioural FFT was removed in
* RX-NEW-1 fix 2026-04-23 it produced wrong-bin peaks and weak
* magnitudes that masked real correctness checks. See git history.)
* *
* Interface contract (from matched_filter_multi_segment.v line 361): * Interface contract (from matched_filter_multi_segment.v line 361):
* .clk, .reset_n * .clk, .reset_n
@@ -64,475 +68,8 @@ module matched_filter_processing_chain (
output wire [3:0] chain_state output wire [3:0] chain_state
); );
`ifdef SIMULATION
// ============================================================================ // ============================================================================
// PARAMETERS // IMPLEMENTATION Radix-2 DIT FFT via fft_engine
// ============================================================================
localparam FFT_SIZE = `RP_FFT_SIZE; // 2048
localparam ADDR_BITS = `RP_LOG2_FFT_SIZE; // log2(2048) = 11
// State encoding (4-bit, up to 16 states)
localparam [3:0] ST_IDLE = 4'd0;
localparam [3:0] ST_FWD_FFT = 4'd1; // Collect samples + bit-reverse
localparam [3:0] ST_FWD_BUTTERFLY = 4'd2; // Signal FFT butterflies
localparam [3:0] ST_REF_BITREV = 4'd3; // Bit-reverse copy reference
localparam [3:0] ST_REF_BUTTERFLY = 4'd4; // Reference FFT butterflies
localparam [3:0] ST_MULTIPLY = 4'd5; // Conjugate multiply
localparam [3:0] ST_INV_BITREV = 4'd6; // Bit-reverse copy product
localparam [3:0] ST_INV_BUTTERFLY = 4'd7; // IFFT butterflies + scale
localparam [3:0] ST_OUTPUT = 4'd8; // Stream results
localparam [3:0] ST_DONE = 4'd9; // Return to idle
reg [3:0] state;
// ============================================================================
// SIGNAL BUFFERS
// ============================================================================
// Input sample counter
reg [ADDR_BITS:0] fwd_in_count; // 0..FFT_SIZE
reg fwd_frame_done; // All FFT_SIZE samples received
// Signal time-domain buffer
reg signed [15:0] fwd_buf_i [0:FFT_SIZE-1];
reg signed [15:0] fwd_buf_q [0:FFT_SIZE-1];
// Signal FFT output (frequency domain)
reg signed [15:0] fwd_out_i [0:FFT_SIZE-1];
reg signed [15:0] fwd_out_q [0:FFT_SIZE-1];
reg fwd_out_valid;
// Reference time-domain buffer
reg signed [15:0] ref_buf_i [0:FFT_SIZE-1];
reg signed [15:0] ref_buf_q [0:FFT_SIZE-1];
// Reference FFT output (frequency domain)
reg signed [15:0] ref_fft_i [0:FFT_SIZE-1];
reg signed [15:0] ref_fft_q [0:FFT_SIZE-1];
// ============================================================================
// CONJUGATE MULTIPLY OUTPUT
// ============================================================================
reg signed [15:0] mult_out_i [0:FFT_SIZE-1];
reg signed [15:0] mult_out_q [0:FFT_SIZE-1];
reg mult_done;
// ============================================================================
// INVERSE FFT OUTPUT
// ============================================================================
reg signed [15:0] ifft_out_i [0:FFT_SIZE-1];
reg signed [15:0] ifft_out_q [0:FFT_SIZE-1];
reg ifft_done;
// Output streaming
reg [ADDR_BITS:0] out_count;
reg out_valid_reg;
reg signed [15:0] out_i_reg, out_q_reg;
// ============================================================================
// BEHAVIORAL RADIX-2 DIT FFT (simulation only)
// ============================================================================
// Working arrays for FFT computation (shared between fwd, ref, and inv FFTs)
reg signed [31:0] work_re [0:FFT_SIZE-1];
reg signed [31:0] work_im [0:FFT_SIZE-1];
// Bit-reverse function
function [ADDR_BITS-1:0] bit_reverse;
input [ADDR_BITS-1:0] val;
integer b;
begin
bit_reverse = 0;
for (b = 0; b < ADDR_BITS; b = b + 1)
bit_reverse[ADDR_BITS-1-b] = val[b];
end
endfunction
// FFT computation variables
integer fft_stage, fft_k, fft_j, fft_half, fft_span;
integer fft_idx_even, fft_idx_odd;
reg signed [31:0] tw_re, tw_im;
reg signed [31:0] t_re, t_im;
reg signed [31:0] u_re, u_im;
real tw_angle;
// ============================================================================
// MAIN STATE MACHINE
// ============================================================================
integer i;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
state <= ST_IDLE;
fwd_in_count <= 0;
fwd_frame_done <= 0;
fwd_out_valid <= 0;
mult_done <= 0;
ifft_done <= 0;
out_count <= 0;
out_valid_reg <= 0;
out_i_reg <= 16'd0;
out_q_reg <= 16'd0;
end else begin
// Defaults
out_valid_reg <= 1'b0;
case (state)
// ================================================================
// IDLE: Wait for valid ADC data, start collecting 2048 samples
// ================================================================
ST_IDLE: begin
fwd_in_count <= 0;
fwd_frame_done <= 0;
fwd_out_valid <= 0;
mult_done <= 0;
ifft_done <= 0;
out_count <= 0;
if (adc_valid) begin
// Store first sample (signal + reference)
fwd_buf_i[0] <= $signed(adc_data_i);
fwd_buf_q[0] <= $signed(adc_data_q);
ref_buf_i[0] <= $signed(ref_chirp_real);
ref_buf_q[0] <= $signed(ref_chirp_imag);
fwd_in_count <= 1;
state <= ST_FWD_FFT;
end
end
// ================================================================
// FWD_FFT: Collect remaining samples, then bit-reverse copy signal
// (2048 samples total)
// ================================================================
ST_FWD_FFT: begin
if (!fwd_frame_done) begin
// Still collecting samples
if (adc_valid && fwd_in_count < FFT_SIZE) begin
fwd_buf_i[fwd_in_count] <= $signed(adc_data_i);
fwd_buf_q[fwd_in_count] <= $signed(adc_data_q);
ref_buf_i[fwd_in_count] <= $signed(ref_chirp_real);
ref_buf_q[fwd_in_count] <= $signed(ref_chirp_imag);
fwd_in_count <= fwd_in_count + 1;
end
if (fwd_in_count == FFT_SIZE) begin
fwd_frame_done <= 1;
// Bit-reverse copy SIGNAL into work arrays (via <=)
for (i = 0; i < FFT_SIZE; i = i + 1) begin
work_re[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{fwd_buf_i[i][15]}}, fwd_buf_i[i]};
work_im[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{fwd_buf_q[i][15]}}, fwd_buf_q[i]};
end
end
end else begin
// Bit-reverse copy settled on previous clock.
// Now transition to butterfly computation.
state <= ST_FWD_BUTTERFLY;
end
end
// ================================================================
// FWD_BUTTERFLY: Forward FFT of signal (all stages, simulation only)
// ================================================================
ST_FWD_BUTTERFLY: begin
// In-place radix-2 DIT butterflies (blocking assignments)
for (fft_stage = 0; fft_stage < ADDR_BITS; fft_stage = fft_stage + 1) begin
fft_half = 1 << fft_stage;
fft_span = fft_half << 1;
for (fft_k = 0; fft_k < FFT_SIZE; fft_k = fft_k + fft_span) begin
for (fft_j = 0; fft_j < fft_half; fft_j = fft_j + 1) begin
fft_idx_even = fft_k + fft_j;
fft_idx_odd = fft_idx_even + fft_half;
tw_angle = -2.0 * 3.14159265358979 * fft_j / (fft_span * 1.0);
tw_re = $rtoi($cos(tw_angle) * 32767.0);
tw_im = $rtoi($sin(tw_angle) * 32767.0);
t_re = (work_re[fft_idx_odd] * tw_re - work_im[fft_idx_odd] * tw_im) >>> 15;
t_im = (work_re[fft_idx_odd] * tw_im + work_im[fft_idx_odd] * tw_re) >>> 15;
u_re = work_re[fft_idx_even];
u_im = work_im[fft_idx_even];
work_re[fft_idx_even] = u_re + t_re;
work_im[fft_idx_even] = u_im + t_im;
work_re[fft_idx_odd] = u_re - t_re;
work_im[fft_idx_odd] = u_im - t_im;
end
end
end
// Copy signal FFT results to fwd_out (saturate to 16-bit)
for (i = 0; i < FFT_SIZE; i = i + 1) begin
if (work_re[i] > 32767)
fwd_out_i[i] <= 16'sh7FFF;
else if (work_re[i] < -32768)
fwd_out_i[i] <= 16'sh8000;
else
fwd_out_i[i] <= work_re[i][15:0];
if (work_im[i] > 32767)
fwd_out_q[i] <= 16'sh7FFF;
else if (work_im[i] < -32768)
fwd_out_q[i] <= 16'sh8000;
else
fwd_out_q[i] <= work_im[i][15:0];
end
fwd_out_valid <= 1;
state <= ST_REF_BITREV;
`ifdef SIMULATION
$display("[MF_CHAIN] Forward FFT complete");
`endif
end
// ================================================================
// REF_BITREV: Bit-reverse copy reference into work arrays
// ================================================================
ST_REF_BITREV: begin
for (i = 0; i < FFT_SIZE; i = i + 1) begin
work_re[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{ref_buf_i[i][15]}}, ref_buf_i[i]};
work_im[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{ref_buf_q[i][15]}}, ref_buf_q[i]};
end
state <= ST_REF_BUTTERFLY;
end
// ================================================================
// REF_BUTTERFLY: Forward FFT of reference (same algorithm as signal)
// ================================================================
ST_REF_BUTTERFLY: begin
for (fft_stage = 0; fft_stage < ADDR_BITS; fft_stage = fft_stage + 1) begin
fft_half = 1 << fft_stage;
fft_span = fft_half << 1;
for (fft_k = 0; fft_k < FFT_SIZE; fft_k = fft_k + fft_span) begin
for (fft_j = 0; fft_j < fft_half; fft_j = fft_j + 1) begin
fft_idx_even = fft_k + fft_j;
fft_idx_odd = fft_idx_even + fft_half;
tw_angle = -2.0 * 3.14159265358979 * fft_j / (fft_span * 1.0);
tw_re = $rtoi($cos(tw_angle) * 32767.0);
tw_im = $rtoi($sin(tw_angle) * 32767.0);
t_re = (work_re[fft_idx_odd] * tw_re - work_im[fft_idx_odd] * tw_im) >>> 15;
t_im = (work_re[fft_idx_odd] * tw_im + work_im[fft_idx_odd] * tw_re) >>> 15;
u_re = work_re[fft_idx_even];
u_im = work_im[fft_idx_even];
work_re[fft_idx_even] = u_re + t_re;
work_im[fft_idx_even] = u_im + t_im;
work_re[fft_idx_odd] = u_re - t_re;
work_im[fft_idx_odd] = u_im - t_im;
end
end
end
// Copy reference FFT results to ref_fft (saturate to 16-bit)
for (i = 0; i < FFT_SIZE; i = i + 1) begin
if (work_re[i] > 32767)
ref_fft_i[i] <= 16'sh7FFF;
else if (work_re[i] < -32768)
ref_fft_i[i] <= 16'sh8000;
else
ref_fft_i[i] <= work_re[i][15:0];
if (work_im[i] > 32767)
ref_fft_q[i] <= 16'sh7FFF;
else if (work_im[i] < -32768)
ref_fft_q[i] <= 16'sh8000;
else
ref_fft_q[i] <= work_im[i][15:0];
end
state <= ST_MULTIPLY;
`ifdef SIMULATION
$display("[MF_CHAIN] Reference FFT complete");
`endif
end
// ================================================================
// MULTIPLY: Conjugate multiply FFT(signal) x conj(FFT(reference))
// (a+jb)(c-jd) = (ac+bd) + j(bc-ad)
// Uses fwd_out (signal FFT) and ref_fft (reference FFT)
// ================================================================
ST_MULTIPLY: begin
for (i = 0; i < FFT_SIZE; i = i + 1) begin : mult_loop
reg signed [31:0] a, b, c, d;
reg signed [31:0] ac, bd, bc, ad;
reg signed [31:0] re_result, im_result;
a = {{16{fwd_out_i[i][15]}}, fwd_out_i[i]};
b = {{16{fwd_out_q[i][15]}}, fwd_out_q[i]};
c = {{16{ref_fft_i[i][15]}}, ref_fft_i[i]};
d = {{16{ref_fft_q[i][15]}}, ref_fft_q[i]};
ac = (a * c) >>> 15;
bd = (b * d) >>> 15;
bc = (b * c) >>> 15;
ad = (a * d) >>> 15;
re_result = ac + bd;
im_result = bc - ad;
// Saturate
if (re_result > 32767)
mult_out_i[i] <= 16'sh7FFF;
else if (re_result < -32768)
mult_out_i[i] <= 16'sh8000;
else
mult_out_i[i] <= re_result[15:0];
if (im_result > 32767)
mult_out_q[i] <= 16'sh7FFF;
else if (im_result < -32768)
mult_out_q[i] <= 16'sh8000;
else
mult_out_q[i] <= im_result[15:0];
end
mult_done <= 1;
state <= ST_INV_BITREV;
`ifdef SIMULATION
$display("[MF_CHAIN] Conjugate multiply complete");
`endif
end
// ================================================================
// INV_BITREV: Bit-reverse copy conjugate-multiply product
// ================================================================
ST_INV_BITREV: begin
for (i = 0; i < FFT_SIZE; i = i + 1) begin
work_re[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{mult_out_i[i][15]}}, mult_out_i[i]};
work_im[bit_reverse(i[ADDR_BITS-1:0])] <= {{16{mult_out_q[i][15]}}, mult_out_q[i]};
end
state <= ST_INV_BUTTERFLY;
end
// ================================================================
// INV_BUTTERFLY: IFFT butterflies (positive twiddle) + 1/N scaling
// ================================================================
ST_INV_BUTTERFLY: begin
for (fft_stage = 0; fft_stage < ADDR_BITS; fft_stage = fft_stage + 1) begin
fft_half = 1 << fft_stage;
fft_span = fft_half << 1;
for (fft_k = 0; fft_k < FFT_SIZE; fft_k = fft_k + fft_span) begin
for (fft_j = 0; fft_j < fft_half; fft_j = fft_j + 1) begin
fft_idx_even = fft_k + fft_j;
fft_idx_odd = fft_idx_even + fft_half;
// IFFT twiddle: +2*pi (positive exponent for inverse)
tw_angle = +2.0 * 3.14159265358979 * fft_j / (fft_span * 1.0);
tw_re = $rtoi($cos(tw_angle) * 32767.0);
tw_im = $rtoi($sin(tw_angle) * 32767.0);
t_re = (work_re[fft_idx_odd] * tw_re - work_im[fft_idx_odd] * tw_im) >>> 15;
t_im = (work_re[fft_idx_odd] * tw_im + work_im[fft_idx_odd] * tw_re) >>> 15;
u_re = work_re[fft_idx_even];
u_im = work_im[fft_idx_even];
work_re[fft_idx_even] = u_re + t_re;
work_im[fft_idx_even] = u_im + t_im;
work_re[fft_idx_odd] = u_re - t_re;
work_im[fft_idx_odd] = u_im - t_im;
end
end
end
// Scale by 1/N (right shift by log2(2048) = 11) and store
for (i = 0; i < FFT_SIZE; i = i + 1) begin : ifft_scale
reg signed [31:0] scaled_re, scaled_im;
scaled_re = work_re[i] >>> ADDR_BITS;
scaled_im = work_im[i] >>> ADDR_BITS;
if (scaled_re > 32767)
ifft_out_i[i] <= 16'sh7FFF;
else if (scaled_re < -32768)
ifft_out_i[i] <= 16'sh8000;
else
ifft_out_i[i] <= scaled_re[15:0];
if (scaled_im > 32767)
ifft_out_q[i] <= 16'sh7FFF;
else if (scaled_im < -32768)
ifft_out_q[i] <= 16'sh8000;
else
ifft_out_q[i] <= scaled_im[15:0];
end
ifft_done <= 1;
state <= ST_OUTPUT;
`ifdef SIMULATION
$display("[MF_CHAIN] Inverse FFT complete range profile ready");
`endif
end
// ================================================================
// OUTPUT: Stream out 2048 range profile samples, one per clock
// ================================================================
ST_OUTPUT: begin
if (out_count < FFT_SIZE) begin
out_i_reg <= ifft_out_i[out_count];
out_q_reg <= ifft_out_q[out_count];
out_valid_reg <= 1'b1;
out_count <= out_count + 1;
end else begin
state <= ST_DONE;
end
end
// ================================================================
// DONE: Return to idle, ready for next frame
// ================================================================
ST_DONE: begin
state <= ST_IDLE;
`ifdef SIMULATION
$display("[MF_CHAIN] Frame complete, returning to IDLE");
`endif
end
default: state <= ST_IDLE;
endcase
end
end
// ============================================================================
// OUTPUT ASSIGNMENTS
// ============================================================================
assign range_profile_i = out_i_reg;
assign range_profile_q = out_q_reg;
assign range_profile_valid = out_valid_reg;
assign chain_state = state;
// ============================================================================
// BUFFER INITIALIZATION (simulation)
// ============================================================================
integer init_idx;
initial begin
for (init_idx = 0; init_idx < FFT_SIZE; init_idx = init_idx + 1) begin
fwd_buf_i[init_idx] = 16'd0;
fwd_buf_q[init_idx] = 16'd0;
fwd_out_i[init_idx] = 16'd0;
fwd_out_q[init_idx] = 16'd0;
ref_buf_i[init_idx] = 16'd0;
ref_buf_q[init_idx] = 16'd0;
ref_fft_i[init_idx] = 16'd0;
ref_fft_q[init_idx] = 16'd0;
mult_out_i[init_idx] = 16'd0;
mult_out_q[init_idx] = 16'd0;
ifft_out_i[init_idx] = 16'd0;
ifft_out_q[init_idx] = 16'd0;
work_re[init_idx] = 32'd0;
work_im[init_idx] = 32'd0;
end
end
`else
// ============================================================================
// SYNTHESIS IMPLEMENTATION — Radix-2 DIT FFT via fft_engine
// ============================================================================ // ============================================================================
// Uses a single fft_engine instance (2048-pt) reused 3 times: // Uses a single fft_engine instance (2048-pt) reused 3 times:
// 1. Forward FFT of signal // 1. Forward FFT of signal
@@ -1245,6 +782,5 @@ initial begin
end end
end end
`endif
endmodule endmodule
+13 -17
View File
@@ -44,20 +44,16 @@
`include "radar_params.vh" `include "radar_params.vh"
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
// !!! 200T 20 km MODE BROKEN — FIX BEFORE 200T BRING-UP !!! // [RX-D FIX] NUM_RANGE_BINS and range_bin port widths now scale with
// The prev-chirp BRAM buffer is sized to NUM_RANGE_BINS (512) and the // `RP_MAX_OUTPUT_BINS and `RP_RANGE_BIN_WIDTH_MAX (conditional on
// range_bin_in port is 9 bits (`RP_RANGE_BIN_BITS). In 20 km mode the // SUPPORT_LONG_RANGE):
// upstream range_bin_decimator emits `RP_OUTPUT_RANGE_BINS_20KM = 4096 // 50T (no SUPPORT_LONG_RANGE): 512 bins / 9-bit — 3 km only
// bins per chirp (8 segments × 512 decimated bins), which aliases into // 200T (SUPPORT_LONG_RANGE): 4096 bins / 12-bit — supports 20 km mode
// the 9-bit address space and collapses bins 512..4095 onto bins 0..511. // The prev-chirp BRAM buffer auto-resizes accordingly; in 20 km mode all
// On XC7A50T this is latent (SUPPORT_LONG_RANGE undefined → 3 km only), // 4096 range cells are stored without aliasing.
// but on XC7A200T with SUPPORT_LONG_RANGE the 20 km data path will
// silently corrupt every range cell above 3 km.
// Fix before 200T bring-up: scale NUM_RANGE_BINS/range_bin width with
// `RP_MAX_OUTPUT_BINS, or gate MTI off entirely in 20 km mode.
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
module mti_canceller #( module mti_canceller #(
parameter NUM_RANGE_BINS = `RP_NUM_RANGE_BINS, // 512 parameter NUM_RANGE_BINS = `RP_MAX_OUTPUT_BINS, // 512 (50T) / 4096 (200T)
parameter DATA_WIDTH = `RP_DATA_WIDTH // 16 parameter DATA_WIDTH = `RP_DATA_WIDTH // 16
) ( ) (
input wire clk, input wire clk,
@@ -67,13 +63,13 @@ module mti_canceller #(
input wire signed [DATA_WIDTH-1:0] range_i_in, input wire signed [DATA_WIDTH-1:0] range_i_in,
input wire signed [DATA_WIDTH-1:0] range_q_in, input wire signed [DATA_WIDTH-1:0] range_q_in,
input wire range_valid_in, input wire range_valid_in,
input wire [`RP_RANGE_BIN_BITS-1:0] range_bin_in, // 9-bit input wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_in, // 9-bit (50T) / 12-bit (200T)
// ========== OUTPUT (to Doppler processor) ========== // ========== OUTPUT (to Doppler processor) ==========
output reg signed [DATA_WIDTH-1:0] range_i_out, output reg signed [DATA_WIDTH-1:0] range_i_out,
output reg signed [DATA_WIDTH-1:0] range_q_out, output reg signed [DATA_WIDTH-1:0] range_q_out,
output reg range_valid_out, output reg range_valid_out,
output reg [`RP_RANGE_BIN_BITS-1:0] range_bin_out, // 9-bit output reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_out, // 9-bit (50T) / 12-bit (200T)
// ========== CONFIGURATION ========== // ========== CONFIGURATION ==========
input wire mti_enable, // 1=MTI active, 0=pass-through input wire mti_enable, // 1=MTI active, 0=pass-through
@@ -111,7 +107,7 @@ module mti_canceller #(
reg signed [DATA_WIDTH-1:0] range_i_d1, range_q_d1; reg signed [DATA_WIDTH-1:0] range_i_d1, range_q_d1;
reg range_valid_d1; reg range_valid_d1;
reg [`RP_RANGE_BIN_BITS-1:0] range_bin_d1; reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_d1;
reg mti_enable_d1; reg mti_enable_d1;
reg use_long_chirp_d1; reg use_long_chirp_d1;
@@ -120,7 +116,7 @@ always @(posedge clk or negedge reset_n) begin
range_i_d1 <= {DATA_WIDTH{1'b0}}; range_i_d1 <= {DATA_WIDTH{1'b0}};
range_q_d1 <= {DATA_WIDTH{1'b0}}; range_q_d1 <= {DATA_WIDTH{1'b0}};
range_valid_d1 <= 1'b0; range_valid_d1 <= 1'b0;
range_bin_d1 <= {`RP_RANGE_BIN_BITS{1'b0}}; range_bin_d1 <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
mti_enable_d1 <= 1'b0; mti_enable_d1 <= 1'b0;
use_long_chirp_d1 <= 1'b0; use_long_chirp_d1 <= 1'b0;
end else begin end else begin
@@ -211,7 +207,7 @@ always @(posedge clk or negedge reset_n) begin
range_i_out <= {DATA_WIDTH{1'b0}}; range_i_out <= {DATA_WIDTH{1'b0}};
range_q_out <= {DATA_WIDTH{1'b0}}; range_q_out <= {DATA_WIDTH{1'b0}};
range_valid_out <= 1'b0; range_valid_out <= 1'b0;
range_bin_out <= {`RP_RANGE_BIN_BITS{1'b0}}; range_bin_out <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
has_previous <= 1'b0; has_previous <= 1'b0;
mti_first_chirp <= 1'b1; mti_first_chirp <= 1'b1;
prev_chirp_was_long <= 1'b0; prev_chirp_was_long <= 1'b0;
+39 -29
View File
@@ -24,7 +24,7 @@ module radar_receiver_final (
output wire [31:0] doppler_output, output wire [31:0] doppler_output,
output wire doppler_valid, output wire doppler_valid,
output wire [4:0] doppler_bin, output wire [4:0] doppler_bin,
output wire [`RP_RANGE_BIN_BITS-1:0] range_bin, // 9-bit output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin, // 9-bit
// Raw matched-filter output (debug/bring-up) // Raw matched-filter output (debug/bring-up)
output wire signed [15:0] range_profile_i_out, output wire signed [15:0] range_profile_i_out,
@@ -158,9 +158,15 @@ wire doppler_frame_done_level; // raw level from doppler_processor
reg doppler_frame_done_prev; reg doppler_frame_done_prev;
wire doppler_frame_done; // rising-edge pulse (1 clk cycle) wire doppler_frame_done; // rising-edge pulse (1 clk cycle)
// [RX-E FIX] doppler_frame_done_level is HIGH at reset (state==S_IDLE,
// frame_buffer_full==0). Initializing prev to 1'b0 produces a spurious
// rising-edge pulse on cycle 1, before any real frame has been processed,
// which causes a stale AGC gain update and a phantom CFAR tick. Initialize
// prev to 1'b1 so the first edge fires only after the doppler processor
// actually exits idle for a real frame and returns.
always @(posedge clk or negedge reset_n) begin always @(posedge clk or negedge reset_n) begin
if (!reset_n) if (!reset_n)
doppler_frame_done_prev <= 1'b0; doppler_frame_done_prev <= 1'b1;
else else
doppler_frame_done_prev <= doppler_frame_done_level; doppler_frame_done_prev <= doppler_frame_done_level;
end end
@@ -172,13 +178,13 @@ assign doppler_frame_done_out = doppler_frame_done;
wire signed [15:0] decimated_range_i; wire signed [15:0] decimated_range_i;
wire signed [15:0] decimated_range_q; wire signed [15:0] decimated_range_q;
wire decimated_range_valid; wire decimated_range_valid;
wire [`RP_RANGE_BIN_BITS-1:0] decimated_range_bin; // 9-bit wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] decimated_range_bin; // 9-bit
// ========== MTI CANCELLER SIGNALS ========== // ========== MTI CANCELLER SIGNALS ==========
wire signed [15:0] mti_range_i; wire signed [15:0] mti_range_i;
wire signed [15:0] mti_range_q; wire signed [15:0] mti_range_q;
wire mti_range_valid; wire mti_range_valid;
wire [`RP_RANGE_BIN_BITS-1:0] mti_range_bin; // 9-bit wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] mti_range_bin; // 9-bit
wire mti_first_chirp; wire mti_first_chirp;
// ========== RADAR MODE CONTROLLER SIGNALS ========== // ========== RADAR MODE CONTROLLER SIGNALS ==========
@@ -383,28 +389,32 @@ chirp_memory_loader_param chirp_mem (
.mem_ready(mem_ready) .mem_ready(mem_ready)
); );
// 4. CRITICAL: Reference Chirp Latency Buffer // 4. [RX-B FIX, Option A 2026-04-23] Reference chirp wired to MF chain with
// This aligns reference data with FFT output (3187 cycle delay) // a single-FF alignment delay. Previously ran through `latency_buffer` with
// TODO: verify empirically during hardware bring-up with correlation test // LATENCY=3187 — that module is a count-N-valid-pulses-then-prime FIFO,
wire [15:0] delayed_ref_i, delayed_ref_q; // not a true cycle delay. It needed ~2 frames of mem_request pulses before
wire mem_ready_delayed; // any ref reached the chain (so frame 1 saw all-zero ref → noise output).
// Removed in favour of a direct-wire path with one FF.
latency_buffer #( //
.DATA_WIDTH(32), // 16-bit I + 16-bit Q // Why the 1-FF stage: multi_segment ST_PROCESSING latches `adc_data` through
.LATENCY(3187) // one register stage (`fft_input_i <= buf_rdata_i`) before it reaches the
) ref_latency_buffer ( // chain. The ref path from chirp_memory_loader is combinational into the
.clk(clk), // chain. Without compensation, ref leads sig by 1 cycle → autocorrelation
.reset_n(reset_n), // peak at bin 1 instead of bin 0 (verified in tb/tb_rxb_fullchain_latency.v
.data_in({ref_i, ref_q}), // against fft_engine.v synthesis path: peak/mean ratio ~80× confirms clean
.valid_in(mem_request), // correlation; peak position fixed to bin 0 by this register stage).
.data_out({delayed_ref_i, delayed_ref_q}), reg [15:0] ref_chirp_real_d, ref_chirp_imag_d;
.valid_out(mem_ready_delayed) always @(posedge clk or negedge reset_n) begin
); if (!reset_n) begin
ref_chirp_real_d <= 16'd0;
// Assign delayed reference signals (single pair — chirp_memory_loader_param ref_chirp_imag_d <= 16'd0;
// selects long/short reference upstream via use_long_chirp) end else begin
assign ref_chirp_real = delayed_ref_i; ref_chirp_real_d <= ref_i;
assign ref_chirp_imag = delayed_ref_q; ref_chirp_imag_d <= ref_q;
end
end
assign ref_chirp_real = ref_chirp_real_d;
assign ref_chirp_imag = ref_chirp_imag_d;
// 5. Dual Chirp Matched Filter // 5. Dual Chirp Matched Filter
@@ -449,7 +459,7 @@ matched_filter_multi_segment mf_dual (
// Convert 2048 range bins to 512 bins for Doppler // Convert 2048 range bins to 512 bins for Doppler
range_bin_decimator #( range_bin_decimator #(
.INPUT_BINS(`RP_FFT_SIZE), // 2048 .INPUT_BINS(`RP_FFT_SIZE), // 2048
.OUTPUT_BINS(`RP_NUM_RANGE_BINS), // 512 .OUTPUT_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.DECIMATION_FACTOR(`RP_DECIMATION_FACTOR) // 4 .DECIMATION_FACTOR(`RP_DECIMATION_FACTOR) // 4
) range_decim ( ) range_decim (
.clk(clk), .clk(clk),
@@ -471,7 +481,7 @@ range_bin_decimator #(
// H(z) = 1 - z^{-1} → null at DC Doppler, removes stationary clutter. // H(z) = 1 - z^{-1} → null at DC Doppler, removes stationary clutter.
// When host_mti_enable=0: transparent pass-through. // When host_mti_enable=0: transparent pass-through.
mti_canceller #( mti_canceller #(
.NUM_RANGE_BINS(`RP_NUM_RANGE_BINS), // 512 .NUM_RANGE_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.DATA_WIDTH(`RP_DATA_WIDTH) // 16 .DATA_WIDTH(`RP_DATA_WIDTH) // 16
) mti_inst ( ) mti_inst (
.clk(clk), .clk(clk),
@@ -528,7 +538,7 @@ assign range_data_valid = mti_range_valid;
// ========== DOPPLER PROCESSOR ========== // ========== DOPPLER PROCESSOR ==========
doppler_processor_optimized #( doppler_processor_optimized #(
.DOPPLER_FFT_SIZE(`RP_DOPPLER_FFT_SIZE), // 16 .DOPPLER_FFT_SIZE(`RP_DOPPLER_FFT_SIZE), // 16
.RANGE_BINS(`RP_NUM_RANGE_BINS), // 512 .RANGE_BINS(`RP_MAX_OUTPUT_BINS), // 512 (50T) / 4096 (200T) [RX-D]
.CHIRPS_PER_FRAME(`RP_CHIRPS_PER_FRAME), // 32 .CHIRPS_PER_FRAME(`RP_CHIRPS_PER_FRAME), // 32
.CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME) // 16 .CHIRPS_PER_SUBFRAME(`RP_CHIRPS_PER_SUBFRAME) // 16
) doppler_proc ( ) doppler_proc (
+4 -4
View File
@@ -127,7 +127,7 @@ module radar_system_top (
output wire [31:0] dbg_doppler_data, output wire [31:0] dbg_doppler_data,
output wire dbg_doppler_valid, output wire dbg_doppler_valid,
output wire [4:0] dbg_doppler_bin, output wire [4:0] dbg_doppler_bin,
output wire [`RP_RANGE_BIN_BITS-1:0] dbg_range_bin, output wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] dbg_range_bin,
// System status // System status
output wire [3:0] system_status, output wire [3:0] system_status,
@@ -181,7 +181,7 @@ wire tx_current_chirp_sync_valid;
wire [31:0] rx_doppler_output; wire [31:0] rx_doppler_output;
wire rx_doppler_valid; wire rx_doppler_valid;
wire [4:0] rx_doppler_bin; wire [4:0] rx_doppler_bin;
wire [`RP_RANGE_BIN_BITS-1:0] rx_range_bin; wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] rx_range_bin;
wire [31:0] rx_range_profile; wire [31:0] rx_range_profile;
wire rx_range_valid; wire rx_range_valid;
wire [15:0] rx_range_profile_decimated; wire [15:0] rx_range_profile_decimated;
@@ -629,7 +629,7 @@ assign dc_notch_active = (host_dc_notch_width != 3'd0) &&
wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output; wire [31:0] notched_doppler_data = dc_notch_active ? 32'd0 : rx_doppler_output;
wire notched_doppler_valid = rx_doppler_valid; wire notched_doppler_valid = rx_doppler_valid;
wire [4:0] notched_doppler_bin = rx_doppler_bin; wire [4:0] notched_doppler_bin = rx_doppler_bin;
wire [`RP_RANGE_BIN_BITS-1:0] notched_range_bin = rx_range_bin; wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] notched_range_bin = rx_range_bin;
// ============================================================================ // ============================================================================
// CFAR DETECTOR (replaces simple threshold detector) // CFAR DETECTOR (replaces simple threshold detector)
@@ -640,7 +640,7 @@ wire [`RP_RANGE_BIN_BITS-1:0] notched_range_bin = rx_range_bin;
wire cfar_detect_flag; wire cfar_detect_flag;
wire cfar_detect_valid; wire cfar_detect_valid;
wire [`RP_RANGE_BIN_BITS-1:0] cfar_detect_range; wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] cfar_detect_range;
wire [4:0] cfar_detect_doppler; wire [4:0] cfar_detect_doppler;
wire [16:0] cfar_detect_magnitude; wire [16:0] cfar_detect_magnitude;
wire [16:0] cfar_detect_threshold; wire [16:0] cfar_detect_threshold;
+8 -4
View File
@@ -32,9 +32,13 @@
`include "radar_params.vh" `include "radar_params.vh"
// [RX-D FIX] OUTPUT_BINS and range_bin_index now scale with
// `RP_MAX_OUTPUT_BINS / `RP_RANGE_BIN_WIDTH_MAX so 20 km mode gets the
// full 4096-bin range axis (8 segments × 512 decimated bins per segment).
// 50T: 512 / 9-bit. 200T: 4096 / 12-bit.
module range_bin_decimator #( module range_bin_decimator #(
parameter INPUT_BINS = `RP_FFT_SIZE, // 2048 parameter INPUT_BINS = `RP_FFT_SIZE, // 2048
parameter OUTPUT_BINS = `RP_NUM_RANGE_BINS, // 512 parameter OUTPUT_BINS = `RP_MAX_OUTPUT_BINS, // 512 (50T) / 4096 (200T)
parameter DECIMATION_FACTOR = `RP_DECIMATION_FACTOR // 4 parameter DECIMATION_FACTOR = `RP_DECIMATION_FACTOR // 4
) ( ) (
input wire clk, input wire clk,
@@ -49,7 +53,7 @@ module range_bin_decimator #(
output reg signed [15:0] range_i_out, output reg signed [15:0] range_i_out,
output reg signed [15:0] range_q_out, output reg signed [15:0] range_q_out,
output reg range_valid_out, output reg range_valid_out,
output reg [`RP_RANGE_BIN_BITS-1:0] range_bin_index, // 9-bit output reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_index, // 9-bit / 12-bit
// Configuration // Configuration
input wire [1:0] decimation_mode, // 00=decimate, 01=peak, 10=average input wire [1:0] decimation_mode, // 00=decimate, 01=peak, 10=average
@@ -82,7 +86,7 @@ reg [10:0] in_bin_count;
// Group tracking // Group tracking
reg [1:0] group_sample_count; // 0..3 within current group of 4 reg [1:0] group_sample_count; // 0..3 within current group of 4
reg [8:0] output_bin_count; // 0..511 output bin index reg [`RP_RANGE_BIN_WIDTH_MAX-1:0] output_bin_count; // 0..OUTPUT_BINS-1
// State machine // State machine
reg [2:0] state; reg [2:0] state;
@@ -146,7 +150,7 @@ always @(posedge clk or negedge reset_n) begin
range_valid_out <= 1'b0; range_valid_out <= 1'b0;
range_i_out <= 16'd0; range_i_out <= 16'd0;
range_q_out <= 16'd0; range_q_out <= 16'd0;
range_bin_index <= {`RP_RANGE_BIN_BITS{1'b0}}; range_bin_index <= {`RP_RANGE_BIN_WIDTH_MAX{1'b0}};
peak_i <= 16'd0; peak_i <= 16'd0;
peak_q <= 16'd0; peak_q <= 16'd0;
peak_mag <= 17'd0; peak_mag <= 17'd0;
@@ -195,7 +195,7 @@ module tb_matched_filter_processing_chain;
integer wait_count; integer wait_count;
begin begin
wait_count = 0; wait_count = 0;
while (chain_state != target_state && wait_count < 50000) begin while (chain_state != target_state && wait_count < 500000) begin
@(posedge clk); @(posedge clk);
wait_count = wait_count + 1; wait_count = wait_count + 1;
end end
@@ -208,7 +208,7 @@ module tb_matched_filter_processing_chain;
integer wait_count; integer wait_count;
begin begin
wait_count = 0; wait_count = 0;
while (chain_state != ST_IDLE && wait_count < 50000) begin while (chain_state != ST_IDLE && wait_count < 500000) begin
@(posedge clk); @(posedge clk);
wait_count = wait_count + 1; wait_count = wait_count + 1;
end end
@@ -332,7 +332,11 @@ module tb_matched_filter_processing_chain;
// noise that scatters energy far from bin 0. Xilinx IP uses full internal // noise that scatters energy far from bin 0. Xilinx IP uses full internal
// precision and passes this correctly in hardware. // precision and passes this correctly in hardware.
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Autocorrelation peak at bin %0d (expected near 0) - behavioral FFT noise, OK with Xilinx IP", cap_peak_bin); // [RX-NEW-1] fft_engine.v is in-house — it IS the production FFT, not
// a behavioural model that gets swapped for Xilinx IP. Wrong-bin peak
// is therefore a real bug in fft_engine / frequency_matched_filter,
// not "behavioral noise". See project memory ledger entry RX-NEW-1.
$display("[FAIL-INFO] Autocorrelation peak at bin %0d (expected 0) fft_engine bug, see RX-NEW-1", cap_peak_bin);
end end
// Behavioral Q15 FFT scatters the peak, so we cannot assert bin // Behavioral Q15 FFT scatters the peak, so we cannot assert bin
// location — but the peak MUST dominate the mean magnitude. This // location — but the peak MUST dominate the mean magnitude. This
@@ -496,7 +500,7 @@ module tb_matched_filter_processing_chain;
check(cap_count == FFT_SIZE, "Case 1: Got 2048 output samples"); check(cap_count == FFT_SIZE, "Case 1: Got 2048 output samples");
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Case 1: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin); $display("[FAIL-INFO] Case 1: peak at bin %0d (expected 0) fft_engine bug, see RX-NEW-1", cap_peak_bin);
end end
begin : p2m_case1 begin : p2m_case1
integer k, sum_abs, mean_abs; integer k, sum_abs, mean_abs;
@@ -538,7 +542,7 @@ module tb_matched_filter_processing_chain;
check(cap_count == FFT_SIZE, "Case 2: Got 2048 output samples"); check(cap_count == FFT_SIZE, "Case 2: Got 2048 output samples");
if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin if (!(cap_peak_bin <= 128 || cap_peak_bin >= FFT_SIZE - 128)) begin
$display("[WARN] Case 2: peak at bin %0d (expected near 0) - behavioral FFT noise", cap_peak_bin); $display("[FAIL-INFO] Case 2: peak at bin %0d (expected near 0) fft_engine bug, see RX-NEW-1", cap_peak_bin);
end end
begin : p2m_case2 begin : p2m_case2
integer k, sum_abs, mean_abs; integer k, sum_abs, mean_abs;
@@ -0,0 +1,309 @@
`timescale 1ns/1ps
`include "radar_params.vh"
// ============================================================================
// tb_rxb_fullchain_latency.v
//
// RX-B verification — Option A (latency_buffer removed, ref direct-wired).
//
// Production wiring this TB mirrors:
// ddc_i/q (test stimulus) -> matched_filter_multi_segment -> chain
// chirp_memory_loader -----direct wire--------------------> chain ref
//
// Tests:
// 1) Pipeline timing: report cycle counts (first ddc_valid -> first
// pc_valid). Confirms FSM advances and produces output.
// 2) Autocorrelation peak position: drive ddc with the SAME short-chirp
// samples that the loader serves up as ref. Output is the chirp
// autocorrelation. Peak should be at bin 0 if ref/signal are aligned
// at the chain. Any shift indicates an alignment error of N cycles.
// ============================================================================
module tb_rxb_fullchain_latency;
localparam CLK_PERIOD = 10.0; // 100 MHz
localparam FFT_SIZE = `RP_FFT_SIZE; // 2048
localparam SHORT_LEN = 50; // matches RP_SHORT_CHIRP_SAMPLES
reg clk;
reg reset_n;
// multi_segment inputs
reg signed [17:0] ddc_i;
reg signed [17:0] ddc_q;
reg ddc_valid;
reg use_long_chirp;
reg [5:0] chirp_counter;
reg mc_new_chirp;
reg mc_new_elevation;
reg mc_new_azimuth;
// multi_segment <-> memory loader interconnect
wire [1:0] segment_request;
wire [10:0] sample_addr_out;
wire mem_request;
wire mem_ready_loader; // direct from loader
// Loader outputs (direct-wired to chain via multi_segment ports)
wire [15:0] ref_i_raw;
wire [15:0] ref_q_raw;
// multi_segment outputs
wire signed [15:0] pc_i;
wire signed [15:0] pc_q;
wire pc_valid;
wire [3:0] ms_status;
// ----- Memory loader -----
chirp_memory_loader_param #(
.DEBUG(0)
) chirp_mem (
.clk (clk),
.reset_n (reset_n),
.segment_select (segment_request),
.mem_request (mem_request),
.use_long_chirp (use_long_chirp),
.sample_addr (sample_addr_out),
.ref_i (ref_i_raw),
.ref_q (ref_q_raw),
.mem_ready (mem_ready_loader)
);
// ----- 1-FF alignment register (mirrors radar_receiver_final.v) -----
// multi_segment ST_PROCESSING latches adc_data through one register
// stage; ref path needs the same to align at chain inputs.
reg [15:0] ref_i_d, ref_q_d;
always @(posedge clk or negedge reset_n) begin
if (!reset_n) begin
ref_i_d <= 16'd0;
ref_q_d <= 16'd0;
end else begin
ref_i_d <= ref_i_raw;
ref_q_d <= ref_q_raw;
end
end
// ----- multi_segment (drives chain internally) -----
matched_filter_multi_segment ms_dut (
.clk (clk),
.reset_n (reset_n),
.ddc_i (ddc_i),
.ddc_q (ddc_q),
.ddc_valid (ddc_valid),
.use_long_chirp (use_long_chirp),
.chirp_counter (chirp_counter),
.mc_new_chirp (mc_new_chirp),
.mc_new_elevation (mc_new_elevation),
.mc_new_azimuth (mc_new_azimuth),
.ref_chirp_real (ref_i_d),
.ref_chirp_imag (ref_q_d),
.segment_request (segment_request),
.sample_addr_out (sample_addr_out),
.mem_request (mem_request),
.mem_ready (mem_ready_loader),
.pc_i_w (pc_i),
.pc_q_w (pc_q),
.pc_valid_w (pc_valid),
.status (ms_status)
);
always #(CLK_PERIOD/2.0) clk = ~clk;
// -------- Cycle counter + first-event capture --------
integer cycle_count;
integer first_ddc_cycle;
integer first_mem_request_cycle;
integer first_pc_valid_cycle;
integer pc_out_count;
reg saw_ddc, saw_mem_req, saw_pc;
// -------- Output capture for peak detection --------
reg signed [15:0] cap_i [0:FFT_SIZE-1];
reg signed [15:0] cap_q [0:FFT_SIZE-1];
always @(posedge clk) begin
if (!reset_n) begin
cycle_count <= 0;
saw_ddc <= 0;
saw_mem_req <= 0;
saw_pc <= 0;
pc_out_count <= 0;
first_ddc_cycle <= 0;
first_mem_request_cycle <= 0;
first_pc_valid_cycle <= 0;
end else begin
cycle_count <= cycle_count + 1;
if (ddc_valid && !saw_ddc) begin
first_ddc_cycle <= cycle_count;
saw_ddc <= 1;
$display("[T=%0t] FIRST ddc_valid at cycle %0d", $time, cycle_count);
end
if (mem_request && !saw_mem_req) begin
first_mem_request_cycle <= cycle_count;
saw_mem_req <= 1;
$display("[T=%0t] FIRST mem_request at cycle %0d", $time, cycle_count);
end
if (pc_valid) begin
if (!saw_pc) begin
first_pc_valid_cycle <= cycle_count;
saw_pc <= 1;
$display("[T=%0t] FIRST pc_valid at cycle %0d", $time, cycle_count);
end
if (pc_out_count < FFT_SIZE) begin
cap_i[pc_out_count] <= pc_i;
cap_q[pc_out_count] <= pc_q;
pc_out_count <= pc_out_count + 1;
end
end
end
end
// -------- Stimulus arrays — load same short-chirp values that loader will serve --------
reg [15:0] stim_chirp_i [0:SHORT_LEN-1];
reg [15:0] stim_chirp_q [0:SHORT_LEN-1];
integer k;
task feed_short_chirp_signal;
// Drive ddc with the chirp samples (autocorrelation: signal == ref).
// Multi_segment will buffer them and zero-pad to FFT_SIZE.
integer j;
begin
for (j = 0; j < SHORT_LEN; j = j + 1) begin
ddc_i <= {{2{stim_chirp_i[j][15]}}, stim_chirp_i[j]}; // sign-ext to 18b
ddc_q <= {{2{stim_chirp_q[j][15]}}, stim_chirp_q[j]};
ddc_valid <= 1'b1;
@(posedge clk);
end
ddc_valid <= 1'b0;
end
endtask
// -------- Peak finding --------
integer peak_bin;
integer peak_abs;
integer mean_abs;
integer abs_val;
integer total_abs;
task find_peak;
integer kk;
integer val_i, val_q;
begin
peak_bin = 0;
peak_abs = 0;
total_abs = 0;
for (kk = 0; kk < FFT_SIZE; kk = kk + 1) begin
val_i = $signed(cap_i[kk]);
val_q = $signed(cap_q[kk]);
abs_val = (val_i < 0 ? -val_i : val_i)
+ (val_q < 0 ? -val_q : val_q);
total_abs = total_abs + abs_val;
if (abs_val > peak_abs) begin
peak_abs = abs_val;
peak_bin = kk;
end
end
mean_abs = total_abs / FFT_SIZE;
end
endtask
initial begin
$dumpfile("tb_rxb_fullchain_latency.vcd");
$dumpvars(0, tb_rxb_fullchain_latency);
clk = 0;
reset_n = 0;
ddc_i = 0;
ddc_q = 0;
ddc_valid = 0;
use_long_chirp = 1'b0; // use SHORT chirp path so loader uses short_chirp_*.mem
chirp_counter = 6'd0;
mc_new_chirp = 1'b0;
mc_new_elevation = 1'b0;
mc_new_azimuth = 1'b0;
// Load the same short-chirp samples the loader will serve as ref,
// so signal == ref → autocorrelation. Peak should be at bin 0 if
// ref/signal alignment is correct.
$readmemh("short_chirp_i.mem", stim_chirp_i, 0, SHORT_LEN-1);
$readmemh("short_chirp_q.mem", stim_chirp_q, 0, SHORT_LEN-1);
$display("[TB] Loaded %0d short-chirp samples for stimulus", SHORT_LEN);
repeat (8) @(posedge clk);
reset_n = 1;
repeat (8) @(posedge clk);
$display("\n=== RX-B Option A verification ===");
$display("Configuration: latency_buffer REMOVED, ref direct-wired");
$display("Path: chirp_memory_loader.ref_i ----> multi_segment.ref_chirp_real");
$display("FFT_SIZE: %0d, SHORT_LEN: %0d", FFT_SIZE, SHORT_LEN);
$display("");
// Pulse mc_new_chirp
$display("[T=%0t] Pulsing mc_new_chirp HIGH...", $time);
@(posedge clk);
#1 mc_new_chirp = 1'b1;
repeat (4) @(posedge clk);
#1 mc_new_chirp = 1'b0;
// Feed signal samples (same as ref → autocorrelation)
feed_short_chirp_signal;
// Wait for FFT_SIZE outputs (or timeout)
for (k = 0; k < 200000; k = k + 1) begin
@(posedge clk);
if (pc_out_count >= FFT_SIZE) k = 200001;
end
$display("\n=== TIMING ===");
if (saw_ddc) $display("First ddc_valid : cycle %0d", first_ddc_cycle);
if (saw_mem_req) $display("First mem_request : cycle %0d", first_mem_request_cycle);
if (saw_pc) $display("First pc_valid : cycle %0d", first_pc_valid_cycle);
$display("pc outputs captured: %0d / %0d", pc_out_count, FFT_SIZE);
if (pc_out_count >= FFT_SIZE) begin
find_peak;
$display("\n=== AUTOCORRELATION RESULT ===");
$display("Peak bin : %0d", peak_bin);
$display("Peak |I|+|Q| : %0d", peak_abs);
$display("Mean |I|+|Q| : %0d", mean_abs);
$display("Peak / mean ratio : ~%0dx",
(mean_abs > 0) ? (peak_abs / mean_abs) : 0);
$display("");
// Run with the SYNTHESIS path (no +define+SIMULATION) to use
// the production fft_engine.v — peak should be exactly at bin 0
// with peak/mean > 50x for the autocorrelation case. The
// SIMULATION path uses an inline behavioural FFT in
// matched_filter_processing_chain.v with documented numerical
// issues (peaks at non-zero bins, weak magnitudes); the
// synthesis path is the production code.
if (pc_out_count >= FFT_SIZE && peak_abs > 2 * mean_abs && peak_bin == 0) begin
$display("[PASS] Frame 1 produces output, peak at bin 0, peak/mean ~%0dx",
(mean_abs > 0) ? (peak_abs / mean_abs) : 0);
$display(" RX-B fully fixed latency_buffer removed + 1-FF align register.");
end else if (pc_out_count >= FFT_SIZE && peak_abs > 2 * mean_abs) begin
$display("[NEAR] Output present, peak/mean OK, but peak at bin %0d (not 0).",
peak_bin);
$display(" If running with +define+SIMULATION, this is the inline");
$display(" behavioural FFT and is expected to fail. Run without it.");
end else if (pc_out_count >= FFT_SIZE) begin
$display("[FAIL] Output present but peak/mean too low no real correlation.");
end
end else begin
$display("\n=== TIMEOUT chain did not produce all outputs ===");
$display("ms_status=%b", ms_status);
end
repeat (1000) @(posedge clk);
$finish;
end
initial begin
#100000000; // 100 ms hard timeout
$display("[ERROR] Hard simulation timeout");
$finish;
end
endmodule
@@ -0,0 +1,181 @@
`timescale 1ns/1ps
`include "radar_params.vh"
// ============================================================================
// tb_rxb_latency_measure.v
//
// Purpose: empirically measure the pipeline latency of
// matched_filter_processing_chain — cycles between the first ADC sample in
// and the first range_profile_valid out — for both the long-chirp path
// (3000 samples padded to FFT_SIZE) and the short-chirp path (50 samples
// padded to FFT_SIZE).
//
// The measured latency is the value LATENCY in latency_buffer should
// compensate for so that ref_chirp_real/imag arrive at the chain in the
// SAME cycle as the corresponding adc_data_i/q.
//
// Note: matched_filter_multi_segment buffers BUFFER_SIZE=2048 samples
// before emitting to the chain regardless of how many active samples are in
// the chirp (zero-pads short chirps). So both paths feed the chain
// FFT_SIZE samples — the chain itself sees no chirp-type difference. This
// test confirms whether a single LATENCY value works for both.
// ============================================================================
module tb_rxb_latency_measure;
localparam CLK_PERIOD = 10.0; // 100 MHz
localparam FFT_SIZE = `RP_FFT_SIZE; // 2048
reg clk;
reg reset_n;
reg signed [15:0] adc_data_i;
reg signed [15:0] adc_data_q;
reg adc_valid;
reg [5:0] chirp_counter;
reg signed [15:0] ref_chirp_real;
reg signed [15:0] ref_chirp_imag;
wire signed [15:0] range_profile_i;
wire signed [15:0] range_profile_q;
wire range_profile_valid;
wire [3:0] chain_state;
matched_filter_processing_chain dut (
.clk (clk),
.reset_n (reset_n),
.adc_data_i (adc_data_i),
.adc_data_q (adc_data_q),
.adc_valid (adc_valid),
.chirp_counter (chirp_counter),
.ref_chirp_real (ref_chirp_real),
.ref_chirp_imag (ref_chirp_imag),
.range_profile_i (range_profile_i),
.range_profile_q (range_profile_q),
.range_profile_valid (range_profile_valid),
.chain_state (chain_state)
);
always #(CLK_PERIOD/2.0) clk = ~clk;
// Measurement state
integer cycle_in_first; // cycle when first adc_valid pulse went HIGH
integer cycle_out_first; // cycle when first range_profile_valid went HIGH
integer cycle_count;
reg saw_first_in;
reg saw_first_out;
always @(posedge clk) begin
if (!reset_n) begin
cycle_count <= 0;
saw_first_in <= 0;
saw_first_out <= 0;
end else begin
cycle_count <= cycle_count + 1;
if (adc_valid && !saw_first_in) begin
cycle_in_first <= cycle_count;
saw_first_in <= 1;
$display("[T=%0t] FIRST adc_valid=1 at cycle %0d", $time, cycle_count);
end
if (range_profile_valid && !saw_first_out) begin
cycle_out_first <= cycle_count;
saw_first_out <= 1;
$display("[T=%0t] FIRST range_profile_valid=1 at cycle %0d", $time, cycle_count);
end
end
end
// Stimulus
integer k;
integer pipeline_latency;
task feed_unit_chirp(input integer n_active_samples);
// Feed FFT_SIZE samples: first n_active_samples are unit-impulse chirp
// (1 at sample 0, 0 elsewhere) — represents a maximally simple input.
// Both adc and ref get the same impulse for autocorrelation.
integer j;
begin
for (j = 0; j < FFT_SIZE; j = j + 1) begin
if (j == 0) begin
adc_data_i <= 16'sd16384; // ~half full-scale
adc_data_q <= 16'sd0;
ref_chirp_real <= 16'sd16384;
ref_chirp_imag <= 16'sd0;
end else begin
adc_data_i <= 16'sd0;
adc_data_q <= 16'sd0;
ref_chirp_real <= 16'sd0;
ref_chirp_imag <= 16'sd0;
end
adc_valid <= 1'b1;
@(posedge clk);
end
adc_valid <= 1'b0;
end
endtask
initial begin
$dumpfile("tb_rxb_latency_measure.vcd");
$dumpvars(0, tb_rxb_latency_measure);
clk = 0;
reset_n = 0;
adc_data_i = 0;
adc_data_q = 0;
adc_valid = 0;
chirp_counter = 6'd0;
ref_chirp_real = 0;
ref_chirp_imag = 0;
repeat (4) @(posedge clk);
reset_n = 1;
repeat (4) @(posedge clk);
$display("\n=== RX-B latency measurement: chain pipeline depth ===");
$display("FFT_SIZE = %0d", FFT_SIZE);
$display("Feeding 2048-sample unit-impulse autocorrelation frame...");
// Two runs: short chirp (50 active) and long chirp (3000 active).
// The chain itself is chirp-agnostic (always processes FFT_SIZE=2048
// samples) — multi_segment upstream zero-pads — so both should give
// identical chain latency. Confirms whether prior review's claim of
// "different LATENCY for short chirp" is real or a misconception.
feed_unit_chirp(50); // active samples; multi_segment zero-pads upstream
// Wait for output to start (poll every cycle, abort if too long)
for (k = 0; k < 60000; k = k + 1) begin
@(posedge clk);
if (saw_first_out) k = 60001; // exit
end
if (saw_first_out) begin
pipeline_latency = cycle_out_first - cycle_in_first;
$display("\n=== RESULT ===");
$display("First adc_valid : cycle %0d", cycle_in_first);
$display("First valid output : cycle %0d", cycle_out_first);
$display("Pipeline latency : %0d cycles", pipeline_latency);
$display("Current LATENCY in latency_buffer: 3187 cycles");
$display("Delta (measured - configured): %0d cycles", pipeline_latency - 3187);
$display("");
$display("Interpretation:");
$display(" - If delta is near 0, LATENCY=3187 is correct.");
$display(" - Note: this measures only the chain's internal pipeline.");
$display(" Full LATENCY also accounts for upstream multi_segment buffer fill.");
end else begin
$display("\n=== TIMEOUT ===");
$display("range_profile_valid never asserted within 60000 cycles");
$display("(behavioural FFT model in fft_engine.v may be much slower than");
$display(" Xilinx FFT IP try Vivado simulation for accurate timing)");
end
// Wait a bit more to see if we get full 2048 outputs
repeat (5000) @(posedge clk);
$finish;
end
// Safety timeout
initial begin
#10000000; // 10 ms simulated time
$display("[ERROR] Simulation timeout at 10 ms");
$finish;
end
endmodule
@@ -88,7 +88,12 @@ module usb_data_interface_ft2232h (
input wire cfar_valid, input wire cfar_valid,
// New inputs for bulk frame protocol (clk domain) // New inputs for bulk frame protocol (clk domain)
input wire [`RP_RANGE_BIN_BITS-1:0] range_bin_in, // 9-bit range bin index // [RX-D] Widened to RP_RANGE_BIN_WIDTH_MAX (9-bit on 50T, 12-bit on 200T)
// to match upstream pipeline. In 3 km mode only bins 0..511 are exercised
// and the frame wire protocol still emits 512×32=16384 cells. 20 km mode
// (4096 bins, 131072 cells) requires a wire-protocol extension before
// bins 512..4095 can be transported to the host.
input wire [`RP_RANGE_BIN_WIDTH_MAX-1:0] range_bin_in,
input wire [4:0] doppler_bin_in, // 5-bit doppler bin index input wire [4:0] doppler_bin_in, // 5-bit doppler bin index
input wire frame_complete, // 1-cycle pulse from radar_receiver_final edge detector input wire frame_complete, // 1-cycle pulse from radar_receiver_final edge detector
+4 -4
View File
@@ -98,10 +98,10 @@ class DemoTarget:
__slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity") __slots__ = ("azimuth", "classification", "id", "range_m", "snr", "velocity")
# Physical range grid: 64 bins x ~24 m/bin = ~1536 m max # Physical range grid: 512 bins x ~6 m/bin = ~3072 m max (3 km mode)
# Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output. # Bin spacing = c / (2 * Fs) * decimation, where Fs = 100 MHz DDC output, decim = 4.
_RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 16 # ~24 m _RANGE_PER_BIN: float = (3e8 / (2 * 100e6)) * 4 # ~6 m
_MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~1536 m _MAX_RANGE: float = _RANGE_PER_BIN * NUM_RANGE_BINS # ~3072 m
def __init__(self, tid: int): def __init__(self, tid: int):
self.id = tid self.id = tid
+16 -6
View File
@@ -43,9 +43,9 @@ STATUS_HEADER_BYTE = 0xBB
DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1 DATA_PACKET_SIZE = 11 # 1 + 4 + 2 + 2 + 1 + 1
STATUS_PACKET_SIZE = 26 # 1 + 24 + 1 STATUS_PACKET_SIZE = 26 # 1 + 24 + 1
NUM_RANGE_BINS = 64 NUM_RANGE_BINS = 512
NUM_DOPPLER_BINS = 32 NUM_DOPPLER_BINS = 32
NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 2048 NUM_CELLS = NUM_RANGE_BINS * NUM_DOPPLER_BINS # 16384
WATERFALL_DEPTH = 64 WATERFALL_DEPTH = 64
@@ -777,6 +777,13 @@ class RadarAcquisition(threading.Thread):
def _ingest_sample(self, sample: dict): def _ingest_sample(self, sample: dict):
"""Place sample into current frame and emit when complete.""" """Place sample into current frame and emit when complete."""
# [GUI-C2 FIX] Use FPGA frame_start bit as the authoritative sync token.
# If FPGA flags frame_start mid-stream (after a USB drop or any glitch),
# finalize whatever we have and re-align to bin (0, 0). Without this the
# count-only sync stays permanently misaligned after a single dropped byte.
if sample.get("frame_start", 0) and self._sample_idx > 0:
self._finalize_frame() # resets _sample_idx to 0 and starts a new frame
rbin = self._sample_idx // NUM_DOPPLER_BINS rbin = self._sample_idx // NUM_DOPPLER_BINS
dbin = self._sample_idx % NUM_DOPPLER_BINS dbin = self._sample_idx % NUM_DOPPLER_BINS
@@ -788,12 +795,15 @@ class RadarAcquisition(threading.Thread):
if sample.get("detection", 0): if sample.get("detection", 0):
self._frame.detections[rbin, dbin] = 1 self._frame.detections[rbin, dbin] = 1
self._frame.detection_count += 1 self._frame.detection_count += 1
# Accumulate FPGA range profile data (matched-filter output) # [GUI-C4 FIX] FPGA emits the same range_i/range_q for all 32 Doppler
# Each sample carries the range_i/range_q for this range bin. # bins of a given range bin (it's the matched-filter range output,
# Accumulate magnitude across Doppler bins for the range profile. # repeated per Doppler cell). Accumulating across all 32 inflates
# the profile 32x. Capture once per range bin at the first Doppler
# cell instead.
if dbin == 0:
ri = int(sample.get("range_i", 0)) ri = int(sample.get("range_i", 0))
rq = int(sample.get("range_q", 0)) rq = int(sample.get("range_q", 0))
self._frame.range_profile[rbin] += abs(ri) + abs(rq) self._frame.range_profile[rbin] = abs(ri) + abs(rq)
self._sample_idx += 1 self._sample_idx += 1
+10 -10
View File
@@ -66,8 +66,8 @@ class TestRadarSettings(unittest.TestCase):
def test_defaults(self): def test_defaults(self):
s = _models().RadarSettings() s = _models().RadarSettings()
self.assertEqual(s.system_frequency, 10.5e9) self.assertEqual(s.system_frequency, 10.5e9)
self.assertEqual(s.coverage_radius, 1536) self.assertEqual(s.coverage_radius, 3072)
self.assertEqual(s.max_distance, 1536) self.assertEqual(s.max_distance, 3072)
class TestGPSData(unittest.TestCase): class TestGPSData(unittest.TestCase):
@@ -430,17 +430,17 @@ class TestWaveformConfig(unittest.TestCase):
self.assertEqual(wc.chirp_duration_s, 30e-6) self.assertEqual(wc.chirp_duration_s, 30e-6)
self.assertEqual(wc.pri_s, 167e-6) self.assertEqual(wc.pri_s, 167e-6)
self.assertEqual(wc.center_freq_hz, 10.5e9) self.assertEqual(wc.center_freq_hz, 10.5e9)
self.assertEqual(wc.n_range_bins, 64) self.assertEqual(wc.n_range_bins, 512)
self.assertEqual(wc.n_doppler_bins, 32) self.assertEqual(wc.n_doppler_bins, 32)
self.assertEqual(wc.chirps_per_subframe, 16) self.assertEqual(wc.chirps_per_subframe, 16)
self.assertEqual(wc.fft_size, 1024) self.assertEqual(wc.fft_size, 2048)
self.assertEqual(wc.decimation_factor, 16) self.assertEqual(wc.decimation_factor, 4)
def test_range_resolution(self): def test_range_resolution(self):
"""range_resolution_m should be ~23.98 m/bin (matched filter, 100 MSPS).""" """range_resolution_m should be ~6.0 m/bin (matched filter, 100 MSPS, decim 4)."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc = WaveformConfig() wc = WaveformConfig()
self.assertAlmostEqual(wc.range_resolution_m, 23.983, places=1) self.assertAlmostEqual(wc.range_resolution_m, 5.996, places=2)
def test_velocity_resolution(self): def test_velocity_resolution(self):
"""velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps).""" """velocity_resolution_mps should be ~5.34 m/s/bin (PRI=167us, 16 chirps)."""
@@ -452,7 +452,7 @@ class TestWaveformConfig(unittest.TestCase):
"""max_range_m = range_resolution * n_range_bins.""" """max_range_m = range_resolution * n_range_bins."""
from v7.models import WaveformConfig from v7.models import WaveformConfig
wc = WaveformConfig() wc = WaveformConfig()
self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 64, places=1) self.assertAlmostEqual(wc.max_range_m, wc.range_resolution_m * 512, places=1)
def test_max_velocity(self): def test_max_velocity(self):
"""max_velocity_mps = velocity_resolution * n_doppler_bins / 2.""" """max_velocity_mps = velocity_resolution * n_doppler_bins / 2."""
@@ -927,9 +927,9 @@ class TestExtractTargetsFromFrame(unittest.TestCase):
"""Detection at range bin 10 → range = 10 * range_resolution.""" """Detection at range bin 10 → range = 10 * range_resolution."""
from v7.processing import extract_targets_from_frame from v7.processing import extract_targets_from_frame
frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0 frame = self._make_frame(det_cells=[(10, 16)]) # dbin=16 = center → vel=0
targets = extract_targets_from_frame(frame, range_resolution=23.983) targets = extract_targets_from_frame(frame, range_resolution=5.996)
self.assertEqual(len(targets), 1) self.assertEqual(len(targets), 1)
self.assertAlmostEqual(targets[0].range, 10 * 23.983, places=1) self.assertAlmostEqual(targets[0].range, 10 * 5.996, places=1)
self.assertAlmostEqual(targets[0].velocity, 0.0, places=2) self.assertAlmostEqual(targets[0].velocity, 0.0, places=2)
def test_velocity_sign(self): def test_velocity_sign(self):
+7 -7
View File
@@ -109,11 +109,11 @@ class RadarSettings:
the actual waveform parameters. the actual waveform parameters.
""" """
system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc) system_frequency: float = 10.5e9 # Hz (carrier, used for velocity calc)
range_resolution: float = 24.0 # Meters per range bin (c/(2*Fs)*decim) range_resolution: float = 6.0 # Meters per range bin (c/(2*Fs)*decim = 1.5*4)
velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform) velocity_resolution: float = 1.0 # m/s per Doppler bin (calibrate to waveform)
max_distance: float = 1536 # Max detection range (m) max_distance: float = 3072 # Max detection range (m), 3 km mode
map_size: float = 2000 # Map display size (m) map_size: float = 4000 # Map display size (m)
coverage_radius: float = 1536 # Map coverage radius (m) coverage_radius: float = 3072 # Map coverage radius (m), 3 km mode
@dataclass @dataclass
@@ -211,11 +211,11 @@ class WaveformConfig:
chirp_duration_s: float = 30e-6 # Long chirp ramp time chirp_duration_s: float = 30e-6 # Long chirp ramp time
pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen) pri_s: float = 167e-6 # Pulse repetition interval (chirp + listen)
center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER) center_freq_hz: float = 10.5e9 # Carrier frequency (radar_scene.py: F_CARRIER)
n_range_bins: int = 64 # After decimation n_range_bins: int = 512 # After decimation (3 km mode; 4096 in 20 km)
n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16) n_doppler_bins: int = 32 # Total Doppler bins (2 sub-frames x 16)
chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame chirps_per_subframe: int = 16 # Chirps in one Doppler sub-frame
fft_size: int = 1024 # Pre-decimation FFT length fft_size: int = 2048 # Pre-decimation FFT length
decimation_factor: int = 16 # 1024 → 64 decimation_factor: int = 4 # 2048 → 512
@property @property
def range_resolution_m(self) -> float: def range_resolution_m(self) -> float:
@@ -31,11 +31,9 @@ minutes.
""" """
from __future__ import annotations from __future__ import annotations
import os
import random import random
import subprocess import subprocess
import sys import sys
import tempfile
from pathlib import Path from pathlib import Path
import pytest import pytest
@@ -131,8 +129,12 @@ def _run_seed(seed: int, vvp: Path, work: Path) -> tuple[int, list[tuple[int, in
f"+csv={csv_path}", f"+csv={csv_path}",
f"+tag=seed{seed:04d}", f"+tag=seed{seed:04d}",
] ]
res = subprocess.run(cmd, cwd=FPGA_DIR, capture_output=True, text=True, check=False, timeout=120) res = subprocess.run(
assert res.returncode == 0, f"vvp exit={res.returncode}\nstdout:\n{res.stdout}\nstderr:\n{res.stderr}" cmd, cwd=FPGA_DIR, capture_output=True, text=True, check=False, timeout=120,
)
assert res.returncode == 0, (
f"vvp exit={res.returncode}\nstdout:\n{res.stdout}\nstderr:\n{res.stderr}"
)
assert csv_path.exists(), ( assert csv_path.exists(), (
f"vvp completed rc=0 but CSV was not produced at {csv_path}\n" f"vvp completed rc=0 but CSV was not produced at {csv_path}\n"
f"cmd: {cmd}\nstdout:\n{res.stdout[-2000:]}\nstderr:\n{res.stderr[-500:]}" f"cmd: {cmd}\nstdout:\n{res.stdout[-2000:]}\nstderr:\n{res.stderr[-500:]}"
@@ -141,7 +143,9 @@ def _run_seed(seed: int, vvp: Path, work: Path) -> tuple[int, list[tuple[int, in
rows = [] rows = []
with csv_path.open() as fh: with csv_path.open() as fh:
header = fh.readline() header = fh.readline()
assert "baseband_i" in header and "baseband_q" in header, f"unexpected CSV header: {header!r}" assert "baseband_i" in header and "baseband_q" in header, (
f"unexpected CSV header: {header!r}"
)
for line in fh: for line in fh:
parts = line.strip().split(",") parts = line.strip().split(",")
if len(parts) != 3: if len(parts) != 3: